use std::fmt;
use crate::error::{PackageError, PptxError, PptxResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PackURI {
uri: String,
}
impl PackURI {
pub fn new(uri: impl Into<String>) -> PptxResult<Self> {
let uri = uri.into();
validate_pack_uri(&uri)?;
Ok(Self { uri })
}
pub(crate) fn from_raw(uri: String) -> Self {
debug_assert!(uri.starts_with('/'));
Self { uri }
}
#[must_use]
pub fn package() -> Self {
Self {
uri: "/".to_string(),
}
}
#[must_use]
pub fn content_types() -> Self {
Self {
uri: "/[Content_Types].xml".to_string(),
}
}
pub fn from_rel_ref(base_uri: &str, relative_ref: &str) -> PptxResult<Self> {
if relative_ref.starts_with('/') {
return Self::new(relative_ref);
}
let joined = if base_uri.ends_with('/') {
format!("{base_uri}{relative_ref}")
} else {
format!("{base_uri}/{relative_ref}")
};
let normalized = normalize_path(&joined);
Self::new(normalized)
}
#[inline]
#[must_use]
pub fn base_uri(&self) -> &str {
if self.uri == "/" {
return "/";
}
match self.uri.rfind('/') {
Some(0) | None => "/",
Some(idx) => &self.uri[..idx],
}
}
#[inline]
#[must_use]
pub fn ext(&self) -> &str {
let filename = self.filename();
filename.rfind('.').map_or("", |idx| &filename[idx + 1..])
}
#[inline]
#[must_use]
pub fn filename(&self) -> &str {
if self.uri == "/" {
return "";
}
self.uri
.rfind('/')
.map_or_else(|| &*self.uri, |idx| &self.uri[idx + 1..])
}
#[inline]
#[must_use]
pub fn membername(&self) -> &str {
if self.uri.len() > 1 {
&self.uri[1..]
} else {
""
}
}
#[must_use]
pub fn relative_ref(&self, base_uri: &str) -> String {
if base_uri == "/" {
return self.uri[1..].to_string();
}
compute_relative_path(base_uri, &self.uri)
}
#[must_use]
pub fn rels_uri(&self) -> Self {
let filename = self.filename();
let rels_filename = format!("{filename}.rels");
let base = self.base_uri();
let rels_path = if base == "/" {
format!("/_rels/{rels_filename}")
} else {
format!("{base}/_rels/{rels_filename}")
};
Self::from_raw(rels_path)
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.uri
}
#[inline]
#[must_use]
pub fn into_string(self) -> String {
self.uri
}
#[must_use]
pub fn idx(&self) -> Option<u32> {
let filename = self.filename();
let name_part = filename.rfind('.').map_or(filename, |idx| &filename[..idx]);
let digit_start = name_part
.rfind(|c: char| !c.is_ascii_digit())
.map_or(0, |i| i + 1);
if digit_start >= name_part.len() {
return None;
}
let digits = &name_part[digit_start..];
if digits.is_empty() {
return None;
}
digits.parse().ok()
}
}
impl fmt::Display for PackURI {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.uri)
}
}
impl AsRef<str> for PackURI {
#[inline]
fn as_ref(&self) -> &str {
&self.uri
}
}
fn validate_pack_uri(path: &str) -> PptxResult<()> {
if !path.starts_with('/') {
return Err(PptxError::Package(PackageError::InvalidPackUri(format!(
"PackURI must begin with '/', got {path:?}"
))));
}
if path == "/" {
return Ok(());
}
if path.ends_with('/') {
return Err(PptxError::Package(PackageError::InvalidPackUri(format!(
"PackURI must not end with '/', got {path:?}"
))));
}
if path.contains("//") {
return Err(PptxError::Package(PackageError::InvalidPackUri(format!(
"PackURI must not contain empty segments ('//'), got {path:?}"
))));
}
if path.contains('\\') {
return Err(PptxError::Package(PackageError::InvalidPackUri(format!(
"PackURI must not contain backslashes, got {path:?}"
))));
}
for segment in path.split('/').skip(1) {
if segment.is_empty() || segment == "." || segment == ".." {
return Err(PptxError::Package(PackageError::InvalidPackUri(format!(
"PackURI contains invalid segment {segment:?} in {path:?}"
))));
}
}
Ok(())
}
pub(super) fn normalize_path(path: &str) -> String {
let mut segments: Vec<&str> = Vec::new();
for segment in path.split('/') {
match segment {
"" | "." => {}
".." => {
segments.pop();
}
s => segments.push(s),
}
}
format!("/{}", segments.join("/"))
}
pub(super) fn compute_relative_path(from: &str, to: &str) -> String {
let from_parts: Vec<&str> = from.split('/').filter(|s| !s.is_empty()).collect();
let to_parts: Vec<&str> = to.split('/').filter(|s| !s.is_empty()).collect();
let common = from_parts
.iter()
.zip(to_parts.iter())
.take_while(|(a, b)| a == b)
.count();
let ups = from_parts.len() - common;
let mut parts: Vec<&str> = vec![".."; ups];
for part in &to_parts[common..] {
parts.push(part);
}
parts.join("/")
}
#[cfg(test)]
mod tests;