1use std::{
2 borrow::Cow,
3 path::{is_separator, Path, PathBuf},
4};
5
6use percent_encoding::percent_decode_str;
7
8use crate::{builtins::BUILTINS, url_to_path::url_to_path, Flags};
9
10#[derive(Debug, PartialEq, Eq, Clone, Copy)]
12pub enum SpecifierType {
13 Esm,
16 Cjs,
18 Url,
21}
22
23#[derive(Debug, Clone, PartialEq, serde::Serialize)]
25#[serde(tag = "kind", content = "value")]
26pub enum SpecifierError {
27 EmptySpecifier,
29 InvalidPackageSpecifier,
31 #[serde(serialize_with = "serialize_url_error")]
33 UrlError(url::ParseError),
34 InvalidFileUrl,
36}
37
38impl From<url::ParseError> for SpecifierError {
39 fn from(value: url::ParseError) -> Self {
40 SpecifierError::UrlError(value)
41 }
42}
43
44fn serialize_url_error<S>(value: &url::ParseError, serializer: S) -> Result<S::Ok, S::Error>
45where
46 S: serde::Serializer,
47{
48 use serde::Serialize;
49 value.to_string().serialize(serializer)
50}
51
52#[derive(PartialEq, Eq, Hash, Clone, Debug)]
54pub enum Specifier<'a> {
55 Relative(Cow<'a, Path>),
57 Absolute(Cow<'a, Path>),
59 Tilde(Cow<'a, Path>),
61 Hash(Cow<'a, str>),
63 Package(Cow<'a, str>, Cow<'a, str>),
65 Builtin(Cow<'a, str>),
67 Url(&'a str),
69}
70
71impl<'a> Specifier<'a> {
72 pub fn parse(
74 specifier: &'a str,
75 specifier_type: SpecifierType,
76 flags: Flags,
77 ) -> Result<(Specifier<'a>, Option<&'a str>), SpecifierError> {
78 if specifier.is_empty() {
79 return Err(SpecifierError::EmptySpecifier);
80 }
81
82 Ok(match specifier.as_bytes()[0] {
83 b'.' => {
84 let specifier = if let Some(specifier) = specifier.strip_prefix("./") {
85 specifier.trim_start_matches('/')
86 } else {
87 specifier
88 };
89 let (path, query) = decode_path(specifier, specifier_type);
90 (Specifier::Relative(path), query)
91 }
92 b'~' => {
93 let mut specifier = &specifier[1..];
94 if !specifier.is_empty() && is_separator(specifier.as_bytes()[0] as char) {
95 specifier = &specifier[1..];
96 }
97 let (path, query) = decode_path(specifier, specifier_type);
98 (Specifier::Tilde(path), query)
99 }
100 b'/' => {
101 if specifier.starts_with("//") && specifier_type == SpecifierType::Url {
102 (Specifier::Url(specifier), None)
104 } else {
105 let (path, query) = decode_path(specifier, specifier_type);
106 (Specifier::Absolute(path), query)
107 }
108 }
109 b'#' => (Specifier::Hash(Cow::Borrowed(&specifier[1..])), None),
110 _ => {
111 match specifier_type {
113 SpecifierType::Url | SpecifierType::Esm => {
114 if let Ok((scheme, rest)) = parse_scheme(specifier) {
116 let (path, rest) = parse_path(rest);
117 let (query, _) = parse_query(rest);
118 match scheme.as_ref() {
119 "npm" if flags.contains(Flags::NPM_SCHEME) => {
120 if BUILTINS.contains(&path) {
121 return Ok((Specifier::Builtin(Cow::Borrowed(path)), None));
122 }
123
124 (
125 parse_package(percent_decode_str(path).decode_utf8_lossy())?,
126 query,
127 )
128 }
129 "node" => {
130 (Specifier::Builtin(Cow::Borrowed(path)), None)
133 }
134 "file" => (
135 Specifier::Absolute(Cow::Owned(url_to_path(specifier)?)),
136 query,
137 ),
138 _ => (Specifier::Url(specifier), None),
139 }
140 } else {
141 let (path, rest) = parse_path(specifier);
144 if specifier_type == SpecifierType::Esm {
145 if BUILTINS.contains(&path) {
146 return Ok((Specifier::Builtin(Cow::Borrowed(path)), None));
147 }
148
149 let (query, _) = parse_query(rest);
150 (
151 parse_package(percent_decode_str(path).decode_utf8_lossy())?,
152 query,
153 )
154 } else {
155 let (path, query) = decode_path(specifier, specifier_type);
156 (Specifier::Relative(path), query)
157 }
158 }
159 }
160 SpecifierType::Cjs => {
161 if let Some(node_prefixed) = specifier.strip_prefix("node:") {
162 return Ok((Specifier::Builtin(Cow::Borrowed(node_prefixed)), None));
163 }
164
165 if BUILTINS.contains(&specifier) {
166 (Specifier::Builtin(Cow::Borrowed(specifier)), None)
167 } else {
168 #[cfg(windows)]
169 if !flags.contains(Flags::ABSOLUTE_SPECIFIERS) {
170 let path = Path::new(specifier);
171 if path.is_absolute() {
172 return Ok((Specifier::Absolute(Cow::Borrowed(path)), None));
173 }
174 }
175
176 (parse_package(Cow::Borrowed(specifier))?, None)
177 }
178 }
179 }
180 }
181 })
182 }
183
184 pub fn to_string(&'a self) -> Cow<'a, str> {
186 match self {
187 Specifier::Relative(path) | Specifier::Absolute(path) | Specifier::Tilde(path) => {
188 path.as_os_str().to_string_lossy()
189 }
190 Specifier::Hash(path) => path.clone(),
191 Specifier::Package(module, subpath) => {
192 if subpath.is_empty() {
193 Cow::Borrowed(module)
194 } else {
195 let mut res = String::with_capacity(module.len() + subpath.len() + 1);
196 res.push_str(module);
197 res.push('/');
198 res.push_str(subpath);
199 Cow::Owned(res)
200 }
201 }
202 Specifier::Builtin(builtin) => Cow::Borrowed(builtin),
203 Specifier::Url(url) => Cow::Borrowed(url),
204 }
205 }
206}
207
208pub(crate) fn parse_scheme(input: &str) -> Result<(Cow<'_, str>, &str), ()> {
211 if input.is_empty() || !input.starts_with(ascii_alpha) {
212 return Err(());
213 }
214 let mut is_lowercase = true;
215 for (i, c) in input.chars().enumerate() {
216 match c {
217 'A'..='Z' => {
218 is_lowercase = false;
219 }
220 'a'..='z' | '0'..='9' | '+' | '-' | '.' => {}
221 ':' => {
222 let scheme = &input[0..i];
223 let rest = &input[i + 1..];
224 return Ok(if is_lowercase {
225 (Cow::Borrowed(scheme), rest)
226 } else {
227 (Cow::Owned(scheme.to_ascii_lowercase()), rest)
228 });
229 }
230 _ => {
231 return Err(());
232 }
233 }
234 }
235
236 Err(())
238}
239
240fn parse_path(input: &str) -> (&str, &str) {
242 if let Some(pos) = input.chars().position(|c| c == '?' || c == '#') {
245 (&input[0..pos], &input[pos..])
246 } else {
247 (input, "")
248 }
249}
250
251fn parse_query(input: &str) -> (Option<&str>, &str) {
253 if !input.is_empty() && input.as_bytes()[0] == b'?' {
254 if let Some(pos) = input.chars().position(|c| c == '#') {
255 (Some(&input[0..pos]), &input[pos..])
256 } else {
257 (Some(input), "")
258 }
259 } else {
260 (None, input)
261 }
262}
263
264#[inline]
266fn ascii_alpha(ch: char) -> bool {
267 ch.is_ascii_alphabetic()
268}
269
270fn parse_package(specifier: Cow<'_, str>) -> Result<Specifier, SpecifierError> {
271 match specifier {
272 Cow::Borrowed(specifier) => {
273 let (module, subpath) = parse_package_specifier(specifier)?;
274 Ok(Specifier::Package(
275 Cow::Borrowed(module),
276 Cow::Borrowed(subpath),
277 ))
278 }
279 Cow::Owned(specifier) => {
280 let (module, subpath) = parse_package_specifier(&specifier)?;
281 Ok(Specifier::Package(
282 Cow::Owned(module.to_owned()),
283 Cow::Owned(subpath.to_owned()),
284 ))
285 }
286 }
287}
288
289pub fn parse_package_specifier(specifier: &str) -> Result<(&str, &str), SpecifierError> {
290 let idx = specifier.chars().position(|p| p == '/');
291 if specifier.starts_with('@') {
292 let idx = idx.ok_or(SpecifierError::InvalidPackageSpecifier)?;
293 if let Some(next) = &specifier[idx + 1..].chars().position(|p| p == '/') {
294 Ok((
295 &specifier[0..idx + 1 + *next],
296 &specifier[idx + *next + 2..],
297 ))
298 } else {
299 Ok((specifier, ""))
300 }
301 } else if let Some(idx) = idx {
302 Ok((&specifier[0..idx], &specifier[idx + 1..]))
303 } else {
304 Ok((specifier, ""))
305 }
306}
307
308pub(crate) fn decode_path(
309 specifier: &str,
310 specifier_type: SpecifierType,
311) -> (Cow<'_, Path>, Option<&str>) {
312 match specifier_type {
313 SpecifierType::Url | SpecifierType::Esm => {
314 let (path, rest) = parse_path(specifier);
315 let (query, _) = parse_query(rest);
316 let path = match percent_decode_str(path).decode_utf8_lossy() {
317 Cow::Borrowed(v) => Cow::Borrowed(Path::new(v)),
318 Cow::Owned(v) => Cow::Owned(PathBuf::from(v)),
319 };
320 (path, query)
321 }
322 SpecifierType::Cjs => (Cow::Borrowed(Path::new(specifier)), None),
323 }
324}
325
326impl<'a> From<&'a str> for Specifier<'a> {
327 fn from(specifier: &'a str) -> Self {
328 Specifier::parse(specifier, SpecifierType::Cjs, Flags::empty())
329 .unwrap()
330 .0
331 }
332}
333
334impl<'de> serde::Deserialize<'de> for Specifier<'static> {
335 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
336 where
337 D: serde::Deserializer<'de>,
338 {
339 use serde::Deserialize;
340 let s: String = Deserialize::deserialize(deserializer)?;
341 Specifier::parse(&s, SpecifierType::Cjs, Flags::empty())
344 .map(|s| match s.0 {
345 Specifier::Relative(a) => Specifier::Relative(Cow::Owned(a.into_owned())),
346 Specifier::Absolute(a) => Specifier::Absolute(Cow::Owned(a.into_owned())),
347 Specifier::Tilde(a) => Specifier::Tilde(Cow::Owned(a.into_owned())),
348 Specifier::Hash(a) => Specifier::Hash(Cow::Owned(a.into_owned())),
349 Specifier::Package(a, b) => {
350 Specifier::Package(Cow::Owned(a.into_owned()), Cow::Owned(b.into_owned()))
351 }
352 Specifier::Builtin(a) => Specifier::Builtin(Cow::Owned(a.into_owned())),
353 Specifier::Url(_) => todo!(),
354 })
355 .map_err(|_| serde::de::Error::custom("Invalid specifier"))
356 }
357}