use std::{
borrow::Cow,
ops::Range,
path::{Path, PathBuf},
sync::LazyLock,
};
use concat_string::concat_string;
use cow_utils::CowUtils;
use memchr::memchr2_iter;
use regex::Regex;
use smallvec::SmallVec;
use sugar_path::SugarPath;
static WINDOWS_PATH_SEPARATOR: &[char] = &['/', '\\'];
fn split_at_query_mark(path: &str) -> (&str, Option<&str>) {
let query_mark_pos = path.find('?');
query_mark_pos.map_or((path, None), |pos| (&path[..pos], Some(&path[pos..])))
}
pub fn absolute_to_request<'b>(context: &str, maybe_absolute_path: &'b str) -> Cow<'b, str> {
if maybe_absolute_path.starts_with('/')
&& maybe_absolute_path.len() > 1
&& maybe_absolute_path.ends_with('/')
{
return Cow::Borrowed(maybe_absolute_path);
}
if !maybe_absolute_path.starts_with('/') && !is_windows_absolute_path(maybe_absolute_path) {
return Cow::Borrowed(maybe_absolute_path);
}
let mut result = String::with_capacity(
context
.len()
.saturating_add(maybe_absolute_path.len())
.saturating_add(2),
);
push_absolute_to_request(context, maybe_absolute_path, &mut result);
Cow::Owned(result)
}
pub fn relative_path_to_request(rel: &str) -> Cow<'_, str> {
if rel.is_empty() {
Cow::Borrowed("./.")
} else if rel == ".." {
Cow::Borrowed("../.")
} else if rel.starts_with("../") {
Cow::Borrowed(rel)
} else {
Cow::Owned(concat_string!("./", rel))
}
}
#[inline]
fn is_windows_absolute_path(path: &str) -> bool {
let bytes = path.as_bytes();
bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\')
}
#[inline]
fn push_relative_path_to_request(rel: &str, out: &mut String) {
if rel.is_empty() {
out.push_str("./.");
} else if rel == ".." {
out.push_str("../.");
} else if rel.starts_with("../") {
out.push_str(rel);
} else {
out.push_str("./");
out.push_str(rel);
}
}
pub fn push_absolute_to_request(context: &str, maybe_absolute_path: &str, out: &mut String) {
if maybe_absolute_path.starts_with('/')
&& maybe_absolute_path.len() > 1
&& maybe_absolute_path.ends_with('/')
{
out.push_str(maybe_absolute_path);
return;
}
if maybe_absolute_path.starts_with('/') {
let (maybe_absolute_resource, query_part) = split_at_query_mark(maybe_absolute_path);
let tmp = Path::new(maybe_absolute_resource).relative(context);
let tmp_path = tmp.to_string_lossy();
push_relative_path_to_request(&tmp_path, out);
if let Some(query_part) = query_part {
out.push_str(query_part);
}
return;
}
if is_windows_absolute_path(maybe_absolute_path) {
let (maybe_absolute_resource, query_part) = split_at_query_mark(maybe_absolute_path);
let relative_resource = maybe_absolute_resource.as_path().relative(context);
let resource = relative_resource.to_string_lossy();
if is_windows_absolute_path(resource.as_ref()) {
out.push_str(resource.as_ref());
} else {
let resource = resource.cow_replace(WINDOWS_PATH_SEPARATOR, "/");
push_relative_path_to_request(resource.as_ref(), out);
}
if let Some(query_part) = query_part {
out.push_str(query_part);
}
return;
}
out.push_str(maybe_absolute_path);
}
fn push_request_to_absolute(context: &str, relative_path: &str, out: &mut String) {
if relative_path.starts_with("./") || relative_path.starts_with("../") {
let relative_path = if relative_path.starts_with("./") {
relative_path
.strip_prefix("./")
.expect("should start with ./")
} else {
relative_path
};
let mut absolute_path = PathBuf::with_capacity(
context
.len()
.saturating_add(relative_path.len())
.saturating_add(1),
);
absolute_path.push(context);
absolute_path.push(relative_path);
out.push_str(&absolute_path.to_string_lossy());
} else {
out.push_str(relative_path);
}
}
fn identifier_segment_ranges(identifier: &str) -> SmallVec<[Range<u32>; 4]> {
let identifier_len =
u32::try_from(identifier.len()).expect("identifier length should fit into u32");
let mut ranges = SmallVec::new();
let mut last = 0;
for index in memchr2_iter(b'|', b'!', identifier.as_bytes()) {
ranges.push(last as u32..index as u32);
last = index + 1;
}
ranges.push(last as u32..identifier_len);
ranges
}
pub fn make_paths_absolute(context: &str, identifier: &str) -> String {
let ranges = identifier_segment_ranges(identifier);
let relative_segment_count = ranges
.iter()
.filter(|range| {
let segment = &identifier[range.start as usize..range.end as usize];
segment.starts_with("./") || segment.starts_with("../")
})
.count();
let result_capacity = context
.len()
.saturating_add(1)
.saturating_mul(relative_segment_count)
.saturating_add(identifier.len());
let mut result = String::with_capacity(result_capacity);
for range in ranges {
let start = range.start as usize;
let end = range.end as usize;
push_request_to_absolute(context, &identifier[start..end], &mut result);
if end < identifier.len() {
result.push(identifier.as_bytes()[end] as char);
}
}
result
}
pub fn make_paths_relative(context: &str, identifier: &str) -> String {
let ranges = identifier_segment_ranges(identifier);
let segment_capacity = context.len().saturating_mul(2).saturating_add(2);
let result_capacity = segment_capacity
.saturating_mul(ranges.len())
.saturating_add(identifier.len());
let mut result = String::with_capacity(result_capacity);
for range in ranges {
let start = range.start as usize;
let end = range.end as usize;
push_absolute_to_request(context, &identifier[start..end], &mut result);
if end < identifier.len() {
result.push(identifier.as_bytes()[end] as char);
}
}
result
}
pub fn strip_zero_width_space_for_fragment(s: &str) -> Cow<'_, str> {
s.cow_replace("\u{200b}#", "#")
}
pub fn insert_zero_width_space_for_fragment(s: &str) -> Cow<'_, str> {
s.cow_replace("#", "\u{200b}#")
}
static REQUEST_TO_ID_REGEX1: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\.\.?/)+").expect("Failed to initialize REQUEST_TO_ID_REGEX1"));
static REQUEST_TO_ID_REGEX2: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(^[.-]|[^a-zA-Z0-9_-])+").expect("Failed to initialize REQUEST_TO_ID_REGEX2")
});
pub fn request_to_id(request: &str) -> String {
REQUEST_TO_ID_REGEX2
.replace_all(&REQUEST_TO_ID_REGEX1.replace(request, ""), "_")
.to_string()
}
#[test]
fn test_push_absolute_to_request() {
let mut out = String::new();
push_absolute_to_request(
"/workspace/app",
"/workspace/app/src/index.js?foo=1",
&mut out,
);
assert_eq!(out, "./src/index.js?foo=1");
let mut out = String::new();
push_absolute_to_request("/workspace/app", "/regexp/", &mut out);
assert_eq!(out, "/regexp/");
let mut out = String::new();
push_absolute_to_request("/workspace/app", "loader", &mut out);
assert_eq!(out, "loader");
}