lcpfs 2026.1.102

LCP File System - A ZFS-inspired copy-on-write filesystem for Rust
// Copyright 2025 LunaOS Contributors
// SPDX-License-Identifier: Apache-2.0

//! Alternate data stream type definitions

use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

/// Stream error types
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StreamError {
    /// File not found
    FileNotFound(u64),
    /// Stream not found
    StreamNotFound(String),
    /// Dataset not found
    DatasetNotFound(String),
    /// Stream already exists
    StreamExists(String),
    /// Invalid stream name
    InvalidName(String),
    /// Name too long
    NameTooLong(usize),
    /// Too many streams
    TooManyStreams(u64),
    /// I/O error
    IoError(String),
    /// Permission denied
    PermissionDenied,
    /// Operation not supported
    NotSupported,
    /// Cannot delete primary stream
    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"),
        }
    }
}

/// Stream type classification
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StreamType {
    /// Primary data stream (unnamed)
    #[default]
    Data,
    /// Extended attributes
    ExtendedAttribute,
    /// Security descriptor
    SecurityDescriptor,
    /// Reparse point data
    ReparsePoint,
    /// Object ID
    ObjectId,
    /// Index allocation
    IndexAllocation,
    /// Bitmap
    Bitmap,
    /// Custom/user-defined
    Custom,
}

impl StreamType {
    /// Get the type name
    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",
        }
    }
}

/// Information about a data stream
#[derive(Debug, Clone)]
pub struct StreamInfo {
    /// Stream name (empty for primary/default stream)
    pub name: String,
    /// Stream type
    pub stream_type: StreamType,
    /// Size in bytes
    pub size: u64,
    /// Allocated size (may be larger due to block alignment)
    pub allocated_size: u64,
    /// Creation time
    pub created: u64,
    /// Last modification time
    pub modified: u64,
    /// Is this the primary (unnamed) stream
    pub is_primary: bool,
    /// Stream attributes
    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(),
        }
    }
}

/// Stream attributes
#[derive(Debug, Clone, Copy, Default)]
pub struct StreamAttributes {
    /// Stream is sparse
    pub sparse: bool,
    /// Stream is compressed
    pub compressed: bool,
    /// Stream is encrypted
    pub encrypted: bool,
    /// Stream is hidden
    pub hidden: bool,
    /// Stream is read-only
    pub read_only: bool,
    /// Stream is system
    pub system: bool,
}

/// Parsed stream path
#[derive(Debug, Clone)]
pub struct ParsedStreamPath {
    /// File path component
    pub file_path: String,
    /// Stream name (None for primary stream)
    pub stream_name: Option<String>,
    /// Stream type (if specified)
    pub stream_type: Option<StreamType>,
}

impl ParsedStreamPath {
    /// Check if this refers to the primary stream
    pub fn is_primary(&self) -> bool {
        self.stream_name.is_none()
    }

    /// Get the full path including stream
    pub fn full_path(&self) -> String {
        match &self.stream_name {
            Some(name) => alloc::format!("{}:{}", self.file_path, name),
            None => self.file_path.clone(),
        }
    }
}

/// Stream enumeration entry
#[derive(Debug, Clone)]
pub struct StreamEntry {
    /// Stream name
    pub name: String,
    /// Stream type
    pub stream_type: StreamType,
    /// Size in bytes
    pub size: u64,
}

/// Stream handle for read/write operations
#[derive(Debug)]
pub struct StreamHandle {
    /// Dataset name
    pub dataset: String,
    /// Object ID
    pub object_id: u64,
    /// Stream name
    pub stream_name: String,
    /// Current position
    pub position: u64,
    /// Open mode
    pub mode: StreamOpenMode,
}

impl StreamHandle {
    /// Create a new stream handle
    pub fn new(dataset: String, object_id: u64, stream_name: String, mode: StreamOpenMode) -> Self {
        Self {
            dataset,
            object_id,
            stream_name,
            position: 0,
            mode,
        }
    }

    /// Seek to position
    pub fn seek(&mut self, position: u64) {
        self.position = position;
    }

    /// Check if readable
    pub fn is_readable(&self) -> bool {
        matches!(self.mode, StreamOpenMode::Read | StreamOpenMode::ReadWrite)
    }

    /// Check if writable
    pub fn is_writable(&self) -> bool {
        matches!(
            self.mode,
            StreamOpenMode::Write | StreamOpenMode::ReadWrite | StreamOpenMode::Append
        )
    }
}

/// Stream open mode
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamOpenMode {
    /// Read only
    Read,
    /// Write only (truncate)
    Write,
    /// Read and write
    ReadWrite,
    /// Append only
    Append,
}

/// Stream copy options
#[derive(Debug, Clone, Default)]
pub struct StreamCopyOptions {
    /// Overwrite existing stream
    pub overwrite: bool,
    /// Preserve attributes
    pub preserve_attributes: bool,
    /// Verify copy with checksum
    pub verify: bool,
}

/// Maximum stream name length
pub const MAX_STREAM_NAME_LEN: usize = 255;

/// Maximum number of streams per file
pub const MAX_STREAMS_PER_FILE: u64 = 65535;

/// Reserved stream name characters
pub const RESERVED_CHARS: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];

/// Validate stream name
pub fn validate_stream_name(name: &str) -> Result<(), StreamError> {
    if name.is_empty() {
        return Ok(()); // Empty name = primary stream
    }

    if name.len() > MAX_STREAM_NAME_LEN {
        return Err(StreamError::NameTooLong(name.len()));
    }

    // Check for reserved characters
    for c in name.chars() {
        if RESERVED_CHARS.contains(&c) {
            return Err(StreamError::InvalidName(alloc::format!(
                "Contains reserved character: '{}'",
                c
            )));
        }
    }

    // Check for reserved names
    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() {
        // Valid names
        assert!(validate_stream_name("").is_ok());
        assert!(validate_stream_name("metadata").is_ok());
        assert!(validate_stream_name("Zone.Identifier").is_ok());

        // Invalid names
        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");
    }
}