use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StreamError {
FileNotFound(u64),
StreamNotFound(String),
DatasetNotFound(String),
StreamExists(String),
InvalidName(String),
NameTooLong(usize),
TooManyStreams(u64),
IoError(String),
PermissionDenied,
NotSupported,
CannotDeletePrimary,
}
impl fmt::Display for StreamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FileNotFound(id) => write!(f, "File not found: {}", id),
Self::StreamNotFound(name) => write!(f, "Stream not found: {}", name),
Self::DatasetNotFound(ds) => write!(f, "Dataset not found: {}", ds),
Self::StreamExists(name) => write!(f, "Stream already exists: {}", name),
Self::InvalidName(name) => write!(f, "Invalid stream name: {}", name),
Self::NameTooLong(len) => write!(f, "Stream name too long: {} bytes", len),
Self::TooManyStreams(max) => write!(f, "Too many streams (max: {})", max),
Self::IoError(msg) => write!(f, "I/O error: {}", msg),
Self::PermissionDenied => write!(f, "Permission denied"),
Self::NotSupported => write!(f, "Operation not supported"),
Self::CannotDeletePrimary => write!(f, "Cannot delete primary data stream"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StreamType {
#[default]
Data,
ExtendedAttribute,
SecurityDescriptor,
ReparsePoint,
ObjectId,
IndexAllocation,
Bitmap,
Custom,
}
impl StreamType {
pub fn name(&self) -> &'static str {
match self {
Self::Data => "$DATA",
Self::ExtendedAttribute => "$EA",
Self::SecurityDescriptor => "$SECURITY",
Self::ReparsePoint => "$REPARSE",
Self::ObjectId => "$OBJECT_ID",
Self::IndexAllocation => "$INDEX",
Self::Bitmap => "$BITMAP",
Self::Custom => "$CUSTOM",
}
}
}
#[derive(Debug, Clone)]
pub struct StreamInfo {
pub name: String,
pub stream_type: StreamType,
pub size: u64,
pub allocated_size: u64,
pub created: u64,
pub modified: u64,
pub is_primary: bool,
pub attributes: StreamAttributes,
}
impl Default for StreamInfo {
fn default() -> Self {
Self {
name: String::new(),
stream_type: StreamType::Data,
size: 0,
allocated_size: 0,
created: 0,
modified: 0,
is_primary: true,
attributes: StreamAttributes::default(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StreamAttributes {
pub sparse: bool,
pub compressed: bool,
pub encrypted: bool,
pub hidden: bool,
pub read_only: bool,
pub system: bool,
}
#[derive(Debug, Clone)]
pub struct ParsedStreamPath {
pub file_path: String,
pub stream_name: Option<String>,
pub stream_type: Option<StreamType>,
}
impl ParsedStreamPath {
pub fn is_primary(&self) -> bool {
self.stream_name.is_none()
}
pub fn full_path(&self) -> String {
match &self.stream_name {
Some(name) => alloc::format!("{}:{}", self.file_path, name),
None => self.file_path.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct StreamEntry {
pub name: String,
pub stream_type: StreamType,
pub size: u64,
}
#[derive(Debug)]
pub struct StreamHandle {
pub dataset: String,
pub object_id: u64,
pub stream_name: String,
pub position: u64,
pub mode: StreamOpenMode,
}
impl StreamHandle {
pub fn new(dataset: String, object_id: u64, stream_name: String, mode: StreamOpenMode) -> Self {
Self {
dataset,
object_id,
stream_name,
position: 0,
mode,
}
}
pub fn seek(&mut self, position: u64) {
self.position = position;
}
pub fn is_readable(&self) -> bool {
matches!(self.mode, StreamOpenMode::Read | StreamOpenMode::ReadWrite)
}
pub fn is_writable(&self) -> bool {
matches!(
self.mode,
StreamOpenMode::Write | StreamOpenMode::ReadWrite | StreamOpenMode::Append
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamOpenMode {
Read,
Write,
ReadWrite,
Append,
}
#[derive(Debug, Clone, Default)]
pub struct StreamCopyOptions {
pub overwrite: bool,
pub preserve_attributes: bool,
pub verify: bool,
}
pub const MAX_STREAM_NAME_LEN: usize = 255;
pub const MAX_STREAMS_PER_FILE: u64 = 65535;
pub const RESERVED_CHARS: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
pub fn validate_stream_name(name: &str) -> Result<(), StreamError> {
if name.is_empty() {
return Ok(()); }
if name.len() > MAX_STREAM_NAME_LEN {
return Err(StreamError::NameTooLong(name.len()));
}
for c in name.chars() {
if RESERVED_CHARS.contains(&c) {
return Err(StreamError::InvalidName(alloc::format!(
"Contains reserved character: '{}'",
c
)));
}
}
let upper = name.to_uppercase();
if upper == "$DATA" || upper.starts_with("$") {
return Err(StreamError::InvalidName(
"Stream names starting with $ are reserved".to_string(),
));
}
Ok(())
}
use alloc::string::ToString;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_stream_name() {
assert!(validate_stream_name("").is_ok());
assert!(validate_stream_name("metadata").is_ok());
assert!(validate_stream_name("Zone.Identifier").is_ok());
assert!(validate_stream_name("a/b").is_err());
assert!(validate_stream_name("$DATA").is_err());
assert!(validate_stream_name("$Custom").is_err());
}
#[test]
fn test_stream_type_name() {
assert_eq!(StreamType::Data.name(), "$DATA");
assert_eq!(StreamType::ExtendedAttribute.name(), "$EA");
}
#[test]
fn test_parsed_stream_path() {
let parsed = ParsedStreamPath {
file_path: "/data/file.txt".to_string(),
stream_name: Some("metadata".to_string()),
stream_type: None,
};
assert!(!parsed.is_primary());
assert_eq!(parsed.full_path(), "/data/file.txt:metadata");
}
}