use std::fmt;
use nextcloud_sdk_core::{ApiPath, UrlError};
use super::validation;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DavPath {
user: String,
relative: String,
api: ApiPath,
}
impl DavPath {
pub fn root(user: impl AsRef<str>) -> Result<Self, UrlError> {
Self::new(user, "")
}
pub fn new(user: impl AsRef<str>, relative: impl AsRef<str>) -> Result<Self, UrlError> {
let user = validation::validate_user(user.as_ref())?.to_owned();
let relative = validation::validate_relative(relative.as_ref())?;
let wire = if relative.is_empty() {
format!("files/{user}")
} else {
format!("files/{user}/{relative}")
};
Ok(Self {
api: ApiPath::dav(wire)?,
user,
relative,
})
}
pub fn user(&self) -> &str {
&self.user
}
pub fn relative(&self) -> &str {
&self.relative
}
pub fn is_root(&self) -> bool {
self.relative.is_empty()
}
pub fn segments(&self) -> impl Iterator<Item = &str> {
self.relative
.split('/')
.filter(|segment| !segment.is_empty())
}
pub fn join(&self, segment: impl AsRef<str>) -> Result<Self, UrlError> {
let segment = segment.as_ref();
let combined = if self.relative.is_empty() {
segment.to_owned()
} else {
format!("{}/{segment}", self.relative)
};
Self::new(&self.user, combined)
}
pub fn as_str(&self) -> &str {
self.api.as_str()
}
pub fn to_api_path(&self) -> ApiPath {
self.api.clone()
}
}
impl fmt::Display for DavPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::DavPath;
#[test]
fn root_covers_the_user_tree() {
let path = DavPath::root("alice").unwrap();
assert!(path.is_root());
assert_eq!(path.as_str(), "/remote.php/dav/files/alice");
}
#[test]
fn nested_paths_keep_their_segments() {
let path = DavPath::new("alice", "Photos/2024/img.jpg").unwrap();
assert_eq!(
path.as_str(),
"/remote.php/dav/files/alice/Photos/2024/img.jpg"
);
assert_eq!(path.segments().count(), 3);
assert!(!path.is_root());
}
#[test]
fn api_path_matches_the_rendered_string() {
let path = DavPath::new("alice", "Photos").unwrap();
assert_eq!(path.to_api_path().as_str(), path.as_str());
}
#[test]
fn rejects_traversal_in_either_part() {
assert!(DavPath::new("alice", "../etc/passwd").is_err());
assert!(DavPath::new("alice", "Photos/../../etc").is_err());
assert!(DavPath::new("..", "Photos").is_err());
}
#[test]
fn rejects_an_absolute_relative_path() {
assert!(DavPath::new("alice", "/files/alice").is_err());
}
#[test]
fn join_appends_one_segment() {
let parent = DavPath::new("alice", "Photos").unwrap();
assert_eq!(parent.join("img.jpg").unwrap().relative(), "Photos/img.jpg");
assert!(parent.join("../secret").is_err());
assert_eq!(
DavPath::root("alice")
.unwrap()
.join("a")
.unwrap()
.relative(),
"a"
);
}
#[test]
fn display_renders_the_wire_path() {
let path = DavPath::new("alice", "a.txt").unwrap();
assert_eq!(path.to_string(), "/remote.php/dav/files/alice/a.txt");
}
}