Files
markbase/vendor/smb-server/src/named_stream.rs
Warren 866d0536c8 Add SMB AAPL Extensions Phase 1-6 + VFS xattr support
Phase 1: AAPL Create Context negotiation
Phase 2: AFP_AfpInfo Stream structure (Finder info + creation time)
Phase 2.5: SMB Named Stream Backend (NamedStreamPath)
Phase 2.6: Backend Named Stream Support in handlers
Phase 2.7: VFS Extended Attributes (get/set/remove/list_xattr)
Phase 4: Time Machine share config (time_machine field)
Phase 5: Server/Volume Capabilities
Phase 6: macOS Unicode mapping (private range ↔ ASCII)

Tests: 174 smb-server tests pass, 52 VFS tests pass
2026-06-22 14:21:53 +08:00

157 lines
4.2 KiB
Rust

//! 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<Self> {
let s = String::from_utf16(units).map_err(|_| SmbError::NameInvalid)?;
Self::parse_from_str(&s)
}
pub fn parse_from_str(s: &str) -> SmbResult<Self> {
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::<SmbPath>()?;
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<u16> {
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");
}
}