ps2mc 0.1.0

A Rust library for reading, extracting, and rewriting PlayStation 2 memory card images.
Documentation
use super::error::Ps2mcError;

pub const ENTRY_SIZE: usize = 512;

// --- 目录与文件模式位掩码 ---
// pub const MODE_PROTECTED: u16 = 0x0008;
pub const MODE_FILE: u16 = 0x0010;
pub const MODE_DIR: u16 = 0x0020;
// pub const MODE_HIDDEN: u16 = 0x2000;
pub const MODE_EXISTS: u16 = 0x8000;

/// PS2 专属的时间戳结构体 (对应 Python 的 __tod_struct)
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Ps2Time {
    pub sec: u8,
    pub min: u8,
    pub hour: u8,
    pub day: u8,
    pub month: u8,
    pub year: u16,
}

impl Ps2Time {
    /// 从 8 字节的原始数据中解析 PS2 时间(格式: <xBBBBBH)
    pub fn from_bytes(bytes: &[u8; 8]) -> Self {
        Self {
            sec: bytes[1],
            min: bytes[2],
            hour: bytes[3],
            day: bytes[4],
            month: bytes[5],
            year: u16::from_le_bytes([bytes[6], bytes[7]]),
        }
    }
}

/// PS2 内存卡目录项 / 文件元数据
#[derive(Debug, Clone)]
pub struct Entry {
    pub mode: u16,
    pub length: usize,
    pub created: Ps2Time,
    pub cluster: u32,
    pub modified: Ps2Time,
    pub name_bytes: [u8; 32],
}

impl Entry {
    /// 从单张 512 字节的切片中解析出一个 Entry 实例
    pub fn from_raw(bytes: &[u8]) -> Result<Self, Ps2mcError> {
        if bytes.len() < ENTRY_SIZE {
            return Err(Ps2mcError::InvalidEntryLength {
                expected: ENTRY_SIZE,
                actual: bytes.len(),
            });
        }

        // 基于 C-Struct 偏移量进行精准切片映射
        let mode = u16::from_le_bytes([bytes[0], bytes[1]]);
        let length = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);

        let mut created_bytes = [0u8; 8];
        created_bytes.copy_from_slice(&bytes[8..16]);
        let created = Ps2Time::from_bytes(&created_bytes);

        let cluster = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);

        let mut modified_bytes = [0u8; 8];
        modified_bytes.copy_from_slice(&bytes[24..32]);
        let modified = Ps2Time::from_bytes(&modified_bytes);

        let mut name_bytes = [0u8; 32];
        name_bytes.copy_from_slice(&bytes[64..96]);

        Ok(Self {
            mode,
            length: length as usize,
            created,
            cluster,
            modified,
            name_bytes,
        })
    }

    /// 批量构建:从一整块 Cluster 字节流中自动切分并构建 Entry 列表
    /// 对应 Python 的 Entry.build(byte_val)
    pub fn build(byte_val: &[u8]) -> Vec<Self> {
        byte_val
            .chunks_exact(ENTRY_SIZE)
            .filter_map(|chunk| Self::from_raw(chunk).ok())
            .collect()
    }

    /// 检查此条目是否是一个有效的目录
    pub fn is_dir(&self) -> bool {
        let mask = MODE_DIR | MODE_EXISTS;
        (self.mode & mask) == mask
    }

    /// 检查此条目是否是一个有效的文件
    pub fn is_file(&self) -> bool {
        let mask = MODE_FILE | MODE_EXISTS;
        (self.mode & mask) == mask
    }

    /// 检查该条目在文件系统中是否存在(未被彻底销毁或属于有效分配状态)
    pub fn is_exists(&self) -> bool {
        (self.mode & MODE_EXISTS) > 0
    }

    /// 解析 PS2 文件名(去除 \0 并优雅兼容 Shift-JIS 编码的游戏保存目录名)
    pub fn decode_name(&self) -> String {
        // 截取到 C 风格以 \0 结尾的位置
        let clean = self.name_bytes.split(|&b| b == 0).next().unwrap();
        if clean.is_empty() {
            return String::new();
        }

        // 优先采用 Shift-JIS 解码(PS2 官方标准),失败则降级到有损 UTF-8
        let (res, _, has_error) = encoding_rs::SHIFT_JIS.decode(clean);
        if has_error {
            String::from_utf8_lossy(clean).into_owned()
        } else {
            res.into_owned()
        }
    }
}