Skip to main content

dolang_vfs/path/
stream.rs

1//! Windows alternate data stream specifiers.
2//!
3//! NTFS names a stream with two parts: a name and a type. They reach this crate
4//! spelled two different ways, and the two grammars are not interchangeable:
5//!
6//! - As a suffix on the final component of a path the caller wrote, such as
7//!   `file.txt:zone` or `file.txt:zone:$DATA`. The type is optional and, when
8//!   present, carries a leading `$`.
9//! - As the raw name reported by an enumeration, such as `:zone:$DATA`. Both
10//!   the leading `:` and the type are mandatory.
11//!
12//! Only the first grammar is part of the public API; the raw form is parsed on
13//! the way in and never handed back out.
14
15use crate::error::{Error, ErrorKind, Result};
16
17/// A borrowed alternate data stream specifier.
18///
19/// The type is stored without its leading `$`.
20#[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    /// Creates a specifier from a stream name and optional type.
28    pub const fn new(name: &'a str, stream_type: Option<&'a str>) -> Self {
29        Self { name, stream_type }
30    }
31
32    /// Returns the stream name.
33    pub const fn name(&self) -> &'a str {
34        self.name
35    }
36
37    /// Returns the stream type, without its leading `$`.
38    pub const fn stream_type(&self) -> Option<&'a str> {
39        self.stream_type
40    }
41
42    /// Converts this specifier into an owned one.
43    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/// An owned alternate data stream specifier.
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53pub struct StreamSpecBuf {
54    name: String,
55    stream_type: Option<String>,
56}
57
58impl StreamSpecBuf {
59    /// Creates a specifier from a stream name and optional type.
60    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    /// Returns the stream name.
68    pub fn name(&self) -> &str {
69        &self.name
70    }
71
72    /// Returns the stream type, without its leading `$`.
73    pub fn stream_type(&self) -> Option<&str> {
74        self.stream_type.as_deref()
75    }
76
77    /// Borrows this specifier.
78    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
98/// Splits a path's final component into its base name and stream specifier.
99///
100/// # Errors
101///
102/// Fails if the component has a stream suffix that does not follow the
103/// `name:stream[:$TYPE]` grammar.
104pub(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
130/// Renders a base name with `spec` appended as a suffix.
131pub(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/// Parses a raw NTFS stream name of the form `:name:$TYPE`.
145///
146/// # Errors
147///
148/// Fails if either mandatory part is missing.
149// Only the Windows direct backend enumerates raw stream names, but the grammar
150// belongs with its sibling above, and its tests are worth running everywhere.
151#[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}