#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PackURI {
uri: String,
}
impl PackURI {
pub fn new<S: Into<String>>(uri: S) -> Result<Self, String> {
let uri = uri.into();
if !uri.starts_with('/') {
return Err(format!("PackURI must begin with slash, got '{}'", uri));
}
Ok(PackURI { uri })
}
pub fn from_rel_ref(base_uri: &str, relative_ref: &str) -> Result<Self, String> {
let joined = Self::join_paths(base_uri, relative_ref);
let normalized = Self::normalize_path(&joined);
Self::new(normalized)
}
pub fn base_uri(&self) -> &str {
if self.uri == "/" {
return "/";
}
if let Some(pos) = self.uri.rfind('/') {
if pos == 0 {
"/"
} else {
&self.uri[..pos]
}
} else {
"/"
}
}
pub fn filename(&self) -> &str {
if let Some(pos) = self.uri.rfind('/') {
&self.uri[pos + 1..]
} else {
""
}
}
pub fn ext(&self) -> &str {
let filename = self.filename();
if let Some(pos) = filename.rfind('.') {
&filename[pos + 1..]
} else {
""
}
}
pub fn idx(&self) -> Option<u32> {
let filename = self.filename();
if filename.is_empty() {
return None;
}
let name_part = if let Some(pos) = filename.rfind('.') {
&filename[..pos]
} else {
filename
};
let mut digit_start = None;
for (i, c) in name_part.chars().enumerate() {
if c.is_ascii_digit() {
if digit_start.is_none() {
digit_start = Some(i);
}
} else if digit_start.is_some() {
digit_start = None;
}
}
if let Some(start) = digit_start
&& start > 0 && start < name_part.len() {
return name_part[start..].parse::<u32>().ok();
}
None
}
pub fn membername(&self) -> &str {
if self.uri == "/" {
""
} else {
&self.uri[1..]
}
}
pub fn relative_ref(&self, base_uri: &str) -> String {
if base_uri == "/" {
return self.membername().to_string();
}
let from_parts: Vec<&str> = base_uri.split('/').filter(|s| !s.is_empty()).collect();
let to_parts: Vec<&str> = self.uri.split('/').filter(|s| !s.is_empty()).collect();
let common = from_parts
.iter()
.zip(to_parts.iter())
.take_while(|(a, b)| a == b)
.count();
let mut result = String::new();
for _ in common..from_parts.len() {
result.push_str("../");
}
for (i, part) in to_parts.iter().enumerate().skip(common) {
if i > common {
result.push('/');
}
result.push_str(part);
}
result
}
pub fn rels_uri(&self) -> Result<PackURI, String> {
let filename = self.filename();
let base_uri = self.base_uri();
let rels_filename = format!("{}.rels", filename);
let rels_uri_str = if base_uri == "/" {
format!("/_rels/{}", rels_filename)
} else {
format!("{}/_rels/{}", base_uri, rels_filename)
};
Self::new(rels_uri_str)
}
pub fn as_str(&self) -> &str {
&self.uri
}
fn join_paths(base: &str, rel: &str) -> String {
if base.ends_with('/') {
format!("{}{}", base, rel)
} else {
format!("{}/{}", base, rel)
}
}
fn normalize_path(path: &str) -> String {
let mut parts = Vec::new();
for part in path.split('/') {
match part {
"" | "." => {
if parts.is_empty() {
parts.push("");
}
}
".." => {
if parts.len() > 1 {
parts.pop();
}
}
_ => {
parts.push(part);
}
}
}
if parts.is_empty() || (parts.len() == 1 && parts[0].is_empty()) {
return "/".to_string();
}
parts.join("/")
}
}
impl std::fmt::Display for PackURI {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.uri)
}
}
impl AsRef<str> for PackURI {
fn as_ref(&self) -> &str {
&self.uri
}
}
pub const PACKAGE_URI: &str = "/";
pub const CONTENT_TYPES_URI: &str = "/[Content_Types].xml";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_packuri_new() {
assert!(PackURI::new("/word/document.xml").is_ok());
assert!(PackURI::new("word/document.xml").is_err());
}
#[test]
fn test_base_uri() {
let uri = PackURI::new("/ppt/slides/slide1.xml").unwrap();
assert_eq!(uri.base_uri(), "/ppt/slides");
let root = PackURI::new("/").unwrap();
assert_eq!(root.base_uri(), "/");
}
#[test]
fn test_filename() {
let uri = PackURI::new("/ppt/slides/slide1.xml").unwrap();
assert_eq!(uri.filename(), "slide1.xml");
let root = PackURI::new("/").unwrap();
assert_eq!(root.filename(), "");
}
#[test]
fn test_ext() {
let uri = PackURI::new("/word/document.xml").unwrap();
assert_eq!(uri.ext(), "xml");
}
#[test]
fn test_idx() {
let uri = PackURI::new("/ppt/slides/slide21.xml").unwrap();
assert_eq!(uri.idx(), Some(21));
let uri = PackURI::new("/ppt/presentation.xml").unwrap();
assert_eq!(uri.idx(), None);
}
#[test]
fn test_membername() {
let uri = PackURI::new("/word/document.xml").unwrap();
assert_eq!(uri.membername(), "word/document.xml");
let root = PackURI::new("/").unwrap();
assert_eq!(root.membername(), "");
}
}