dolang_vfs/path/
stream.rs1use crate::error::{Error, ErrorKind, Result};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct StreamSpec<'a> {
22 name: &'a str,
23 stream_type: Option<&'a str>,
24}
25
26impl<'a> StreamSpec<'a> {
27 pub const fn new(name: &'a str, stream_type: Option<&'a str>) -> Self {
29 Self { name, stream_type }
30 }
31
32 pub const fn name(&self) -> &'a str {
34 self.name
35 }
36
37 pub const fn stream_type(&self) -> Option<&'a str> {
39 self.stream_type
40 }
41
42 pub fn to_spec_buf(&self) -> StreamSpecBuf {
44 StreamSpecBuf {
45 name: self.name.to_owned(),
46 stream_type: self.stream_type.map(str::to_owned),
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53pub struct StreamSpecBuf {
54 name: String,
55 stream_type: Option<String>,
56}
57
58impl StreamSpecBuf {
59 pub fn new(name: impl Into<String>, stream_type: Option<impl Into<String>>) -> Self {
61 Self {
62 name: name.into(),
63 stream_type: stream_type.map(Into::into),
64 }
65 }
66
67 pub fn name(&self) -> &str {
69 &self.name
70 }
71
72 pub fn stream_type(&self) -> Option<&str> {
74 self.stream_type.as_deref()
75 }
76
77 pub fn to_spec(&self) -> StreamSpec<'_> {
79 StreamSpec {
80 name: &self.name,
81 stream_type: self.stream_type.as_deref(),
82 }
83 }
84}
85
86impl<'a> From<StreamSpec<'a>> for StreamSpecBuf {
87 fn from(spec: StreamSpec<'a>) -> Self {
88 spec.to_spec_buf()
89 }
90}
91
92impl From<&crate::file::StreamEntry> for StreamSpecBuf {
93 fn from(entry: &crate::file::StreamEntry) -> Self {
94 Self::new(entry.name(), Some(entry.stream_type()))
95 }
96}
97
98pub(super) fn split_suffix(component: &str) -> Result<(&str, Option<StreamSpec<'_>>)> {
105 let mut parts = component.split(':');
106 let base = parts.next().expect("split always yields one part");
107 let Some(name) = parts.next() else {
108 return Ok((base, None));
109 };
110 let stream_type = parts.next();
111 if parts.next().is_some() {
112 return Err(Error::new(
113 ErrorKind::InvalidInput,
114 "path final component has too many alternate data stream parts",
115 ));
116 }
117 let stream_type = stream_type
118 .map(|stream_type| {
119 stream_type.strip_prefix('$').ok_or_else(|| {
120 Error::new(
121 ErrorKind::InvalidInput,
122 "explicit alternate data stream type must start with `$`",
123 )
124 })
125 })
126 .transpose()?;
127 Ok((base, Some(StreamSpec { name, stream_type })))
128}
129
130pub(super) fn join_suffix(base: &str, spec: Option<StreamSpec<'_>>) -> String {
132 let mut name = base.to_owned();
133 if let Some(spec) = spec {
134 name.push(':');
135 name.push_str(spec.name);
136 if let Some(stream_type) = spec.stream_type {
137 name.push_str(":$");
138 name.push_str(stream_type);
139 }
140 }
141 name
142}
143
144#[cfg_attr(not(windows), allow(dead_code))]
152pub(crate) fn parse_raw_name(raw: &str) -> Result<(String, String)> {
153 let rest = raw
154 .strip_prefix(':')
155 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "stream name missing `:` prefix"))?;
156 let split = rest
157 .rfind(':')
158 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "stream name missing type suffix"))?;
159 let stream_type = rest[split + 1..]
160 .strip_prefix('$')
161 .ok_or_else(|| Error::new(ErrorKind::InvalidData, "stream type missing `$` prefix"))?;
162 Ok((rest[..split].to_owned(), stream_type.to_owned()))
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{join_suffix, parse_raw_name, split_suffix};
168
169 #[test]
170 fn suffix_grammar_accepts_optional_type() {
171 let (base, spec) = split_suffix("file.txt").unwrap();
172 assert_eq!(base, "file.txt");
173 assert!(spec.is_none());
174
175 let (base, spec) = split_suffix("file.txt:zone").unwrap();
176 let spec = spec.unwrap();
177 assert_eq!(base, "file.txt");
178 assert_eq!(spec.name(), "zone");
179 assert_eq!(spec.stream_type(), None);
180
181 let (base, spec) = split_suffix("file.txt:zone:$DATA").unwrap();
182 let spec = spec.unwrap();
183 assert_eq!(base, "file.txt");
184 assert_eq!(spec.name(), "zone");
185 assert_eq!(spec.stream_type(), Some("DATA"));
186
187 let (_, spec) = split_suffix("file.txt::$DATA").unwrap();
188 assert_eq!(spec.unwrap().name(), "");
189 }
190
191 #[test]
192 fn suffix_grammar_rejects_malformed_types_and_extra_parts() {
193 assert!(split_suffix("file.txt:zone:DATA").is_err());
194 assert!(split_suffix("file.txt:a:b:c").is_err());
195 }
196
197 #[test]
198 fn join_suffix_round_trips_split_suffix() {
199 for component in ["file.txt", "file.txt:zone", "file.txt:zone:$DATA"] {
200 let (base, spec) = split_suffix(component).unwrap();
201 assert_eq!(join_suffix(base, spec), component);
202 }
203 }
204
205 #[test]
206 fn raw_names_require_both_delimiters() {
207 assert_eq!(
208 parse_raw_name(":zone:$DATA").unwrap(),
209 ("zone".to_owned(), "DATA".to_owned())
210 );
211 assert_eq!(
212 parse_raw_name("::$DATA").unwrap(),
213 (String::new(), "DATA".to_owned())
214 );
215 assert!(parse_raw_name("zone:$DATA").is_err());
216 assert!(parse_raw_name(":zone").is_err());
217 assert!(parse_raw_name(":zone:DATA").is_err());
218 }
219}