1use std::borrow::Cow;
18use std::fmt;
19
20use super::content_types::ContentTypes;
21
22#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
27pub struct PartName(String);
28
29impl PartName {
30 pub fn from_unchecked(s: impl Into<String>) -> Self {
34 let s = s.into();
35 PartName(if s.starts_with('/') {
36 s
37 } else {
38 format!("/{}", s)
39 })
40 }
41
42 pub fn new(s: &str) -> Result<Self, PartNameError> {
49 if !s.starts_with('/') {
50 return Err(PartNameError::NotAbsolute);
51 }
52 for seg in s.split('/').skip(1) {
53 if seg.is_empty() {
54 return Err(PartNameError::EmptySegment);
55 }
56 if seg == "." || seg == ".." {
57 return Err(PartNameError::RelSegment);
58 }
59 }
60 Ok(PartName(s.to_string()))
61 }
62
63 #[inline]
65 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68 #[inline]
70 pub fn into_string(self) -> String {
71 self.0
72 }
73
74 #[inline]
76 pub fn to_zip_path(&self) -> &str {
77 &self.0[1..]
78 }
79
80 pub fn ext(&self) -> String {
82 match self.0.rfind('.') {
83 Some(i) => self.0[i..].to_ascii_lowercase(),
84 None => String::new(),
85 }
86 }
87
88 pub fn sibling(&self, filename: &str) -> PartName {
90 let p = self.0.rfind('/').unwrap_or(0);
91 PartName(format!("{}/{}", &self.0[..p], filename))
92 }
93
94 pub fn parent(&self) -> Option<PartName> {
96 let p = self.0.rfind('/')?;
97 if p == 0 {
98 return None;
99 }
100 Some(PartName(self.0[..p].to_string()))
101 }
102}
103
104impl fmt::Debug for PartName {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "PartName({})", self.0)
107 }
108}
109
110impl fmt::Display for PartName {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(&self.0)
113 }
114}
115
116impl std::str::FromStr for PartName {
117 type Err = PartNameError;
118 fn from_str(s: &str) -> Result<Self, Self::Err> {
119 PartName::new(s)
120 }
121}
122
123#[derive(Debug, thiserror::Error)]
125pub enum PartNameError {
126 #[error("part name must start with '/'")]
128 NotAbsolute,
129 #[error("part name contains empty segment")]
131 EmptySegment,
132 #[error("part name contains relative segment '.' or '..'")]
134 RelSegment,
135}
136
137pub fn new_part_name(s: &str) -> PartName {
141 PartName::from_unchecked(s)
142}
143
144#[derive(Debug, Clone)]
152pub struct Part {
153 pub partname: PartName,
155 pub content_type: String,
157 pub blob: Vec<u8>,
159}
160
161impl Part {
162 pub fn new<N, C, B>(partname: N, content_type: C, blob: B) -> Self
164 where
165 N: Into<PartName>,
166 C: Into<String>,
167 B: Into<Vec<u8>>,
168 {
169 Part {
170 partname: partname.into(),
171 content_type: content_type.into(),
172 blob: blob.into(),
173 }
174 }
175
176 pub fn blob_text(&self) -> Option<Cow<'_, str>> {
180 match std::str::from_utf8(&self.blob) {
181 Ok(s) => Some(Cow::Borrowed(s)),
182 Err(_) => None,
183 }
184 }
185
186 pub fn len(&self) -> usize {
188 self.blob.len()
189 }
190 pub fn is_empty(&self) -> bool {
192 self.blob.is_empty()
193 }
194
195 pub fn contribute_to(&self, ct: &mut ContentTypes) {
200 let ext = self.partname.ext();
201 if ext == ".rels" {
202 return;
204 }
205 ct.add_override(self.partname.as_str(), &self.content_type);
207 }
208}
209
210impl From<Part> for std::path::PathBuf {
212 fn from(p: Part) -> std::path::PathBuf {
213 std::path::PathBuf::from(p.partname.to_zip_path())
214 }
215}