//! SMB Named Stream Path handling //! //! Reference: MS-SMB2 ยง2.2.13 (Named Streams) //! Named streams are colon-separated: `filename:stream_name:$DATA` use crate::proto::messages::AFPINFO_STREAM_NAME; use crate::error::{SmbError, SmbResult}; use crate::path::SmbPath; pub const STREAM_TYPE_DATA: &[u8] = b"$DATA"; pub const AFP_RESOURCE_STREAM_NAME: &[u8] = b"AFP_Resource"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct NamedStreamPath { base_path: SmbPath, stream_name: String, stream_type: String, } impl NamedStreamPath { pub fn new(base_path: SmbPath, stream_name: String, stream_type: String) -> Self { Self { base_path, stream_name, stream_type, } } pub fn base_path(&self) -> &SmbPath { &self.base_path } pub fn stream_name(&self) -> &str { &self.stream_name } pub fn stream_type(&self) -> &str { &self.stream_type } pub fn is_afp_info(&self) -> bool { self.stream_name == "AFP_AfpInfo" } pub fn is_afp_resource(&self) -> bool { self.stream_name == "AFP_Resource" } pub fn is_data_stream(&self) -> bool { self.stream_type == "$DATA" } pub fn parse_from_utf16(units: &[u16]) -> SmbResult { let s = String::from_utf16(units).map_err(|_| SmbError::NameInvalid)?; Self::parse_from_str(&s) } pub fn parse_from_str(s: &str) -> SmbResult { let trimmed = s .strip_prefix('\\') .or_else(|| s.strip_prefix('/')) .unwrap_or(s); if trimmed.is_empty() { return Err(SmbError::NameInvalid); } let colon_pos = trimmed.find(':'); if colon_pos.is_none() { return Err(SmbError::NameInvalid); } let pos = colon_pos.unwrap(); let base_str = &trimmed[..pos]; let rest = &trimmed[pos + 1..]; let second_colon = rest.find(':'); let (stream_name, stream_type) = match second_colon { Some(pos2) => { let name = &rest[..pos2]; if name.is_empty() || name == "$DATA" { return Err(SmbError::NameInvalid); } (name, &rest[pos2 + 1..]) } None => { if rest.is_empty() || rest == "$DATA" { return Err(SmbError::NameInvalid); } (rest, "$DATA") } }; let base_path = base_str.parse::()?; Ok(Self { base_path, stream_name: stream_name.to_string(), stream_type: stream_type.to_string(), }) } pub fn display(&self) -> String { format!( "{}:{}:{}", self.base_path.display_backslash(), self.stream_name, self.stream_type ) } } #[cfg(test)] mod tests { use super::*; fn utf16(s: &str) -> Vec { s.encode_utf16().collect() } #[test] fn test_parse_named_stream() { let p = NamedStreamPath::parse_from_str("file.txt:AFP_AfpInfo:$DATA").unwrap(); assert_eq!(p.base_path().file_name(), Some("file.txt")); assert_eq!(p.stream_name(), "AFP_AfpInfo"); assert_eq!(p.stream_type(), "$DATA"); assert!(p.is_afp_info()); assert!(p.is_data_stream()); } #[test] fn test_parse_without_type() { let p = NamedStreamPath::parse_from_str("file.txt:AFP_Resource").unwrap(); assert_eq!(p.stream_name(), "AFP_Resource"); assert_eq!(p.stream_type(), "$DATA"); assert!(p.is_afp_resource()); } #[test] fn test_parse_from_utf16() { let units = utf16("file.txt:AFP_AfpInfo:$DATA"); let p = NamedStreamPath::parse_from_utf16(&units).unwrap(); assert!(p.is_afp_info()); } #[test] fn test_reject_empty_stream_name() { assert!(NamedStreamPath::parse_from_str("file.txt::").is_err()); assert!(NamedStreamPath::parse_from_str("file.txt:$DATA").is_err()); } #[test] fn test_display() { let p = NamedStreamPath::parse_from_str("file.txt:AFP_AfpInfo:$DATA").unwrap(); assert_eq!(p.display(), "file.txt:AFP_AfpInfo:$DATA"); } }