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
157 lines
4.2 KiB
Rust
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");
|
|
}
|
|
} |