ps2mc 0.1.0

A Rust library for reading, extracting, and rewriting PlayStation 2 memory card images.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
use std::collections::HashSet;
use std::fs::{File, OpenOptions, create_dir_all};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;

use super::ecc::calculate;
use super::entry::Entry;
use super::error::Ps2mcError;
use super::icon::Icon;
use super::icon_sys::IconSys;
use super::page::Page;
use super::super_block::SuperBlock;

// --- FAT 常量定义 ---
const FAT_ALLOCATED_BIT: u32 = 0x8000_0000;
const FAT_CHAIN_END: u32 = 0x7FFF_FFFF;
const FAT_UNALLOCATED: u32 = 0xFFFF_FFFF;

/// A decoded PlayStation 2 memory card image.
pub struct MemoryCard {
    file: File,
    super_block: SuperBlock,
    cluster_buffer: Box<[u8]>,
    cluster_data_buffer: Box<[u8]>,
    page_buffer: Box<[u8]>,
    fat_clusters: Vec<u32>,
    entries_in_root: Vec<Entry>,
}

/// The parsed icon assets associated with a save directory.
#[derive(Debug, Clone, PartialEq)]
pub struct SaveIconSet {
    /// Parsed `icon.sys`.
    pub icon_sys: IconSys,
    /// Parsed icon payloads referenced by `icon.sys`.
    pub icons: Vec<Icon>,
}

impl MemoryCard {
    /// Opens a writable PS2 memory card image from a filesystem path.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// # let _ = card;
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn open(path: impl AsRef<Path>) -> Result<Self, Ps2mcError> {
        let file = OpenOptions::new().read(true).write(true).open(path)?;
        Self::from_file(file)
    }

    /// Builds a memory-card reader from an already opened file handle.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let file = std::fs::OpenOptions::new()
    ///     .read(true)
    ///     .write(true)
    ///     .open("memcard.ps2")?;
    /// let card = ps2mc::MemoryCard::from_file(file)?;
    /// # let _ = card;
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn from_file(mut file: File) -> Result<Self, Ps2mcError> {
        let mut super_block_bytes = vec![0_u8; 340];
        file.read_exact(&mut super_block_bytes)?;
        let super_block = SuperBlock::read_from(&super_block_bytes)?;
        let raw_page_size = super_block.raw_page_size;
        let raw_cluster_size = raw_page_size * super_block.pages_per_cluster;
        let cluster_size = super_block.cluster_size;
        let mut instance = Self {
            file,
            super_block,
            cluster_buffer: vec![0u8; raw_cluster_size].into_boxed_slice(),
            cluster_data_buffer: vec![0u8; cluster_size].into_boxed_slice(),
            page_buffer: vec![0u8; raw_page_size].into_boxed_slice(),
            fat_clusters: Vec::new(),
            entries_in_root: Vec::new(),
        };
        instance.build_fat()?; // 预先构建 FAT 表,后续读取目录时直接使用
        let root_entry = instance.root_entry()?;
        instance.entries_in_root = instance
            .find_sub_entries(root_entry.cluster, root_entry.length)?
            .into_iter()
            .filter(|e| e.is_exists())
            .collect();
        Ok(instance)
    }

    fn build_fat(&mut self) -> Result<(), Ps2mcError> {
        let indirect_clusters: Vec<u32> = self
            .super_block
            .ifc_list
            .iter()
            .filter(|&&c| c != FAT_UNALLOCATED)
            .copied()
            .collect();

        for &indirect_cluster in &indirect_clusters {
            let indirect_entries: Vec<u32> = {
                let cluster_data = self.read_cluster(indirect_cluster as usize)?;
                cluster_data
                    .chunks_exact(4)
                    .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
                    .filter(|v| *v != FAT_UNALLOCATED)
                    .collect()
            };

            for &entry in &indirect_entries {
                let fat_entries: Vec<u32> = {
                    let cluster_data = self.read_cluster(entry as usize)?;
                    cluster_data
                        .chunks_exact(4)
                        .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
                        .collect()
                };
                self.fat_clusters.extend(fat_entries);
            }
        }
        Ok(())
    }

    fn read_entry_cluster(&mut self, n: usize) -> Result<Vec<Entry>, Ps2mcError> {
        let n = n + self.super_block.alloc_offset; // 计算实际簇索引
        Ok(Entry::build(self.read_cluster(n)?))
    }

    /// 读取单个 Cluster 数据 (自动拼接其下所有的 Page 块)
    fn read_cluster(&mut self, n: usize) -> Result<&[u8], Ps2mcError> {
        let raw_page_size = self.super_block.raw_page_size;
        let page_size = self.super_block.page_size;
        let pages_per_cluster = self.super_block.pages_per_cluster;
        let offset = (n * pages_per_cluster * raw_page_size) as u64;

        // 一次性读取整个簇到 cluster_buffer
        self.file.seek(SeekFrom::Start(offset))?;
        self.file.read_exact(&mut self.cluster_buffer)?;

        // 从 cluster_buffer 切出多个 Page,零拷贝
        for i in 0..pages_per_cluster {
            let start = i * raw_page_size;
            let end = start + raw_page_size;
            let raw_slice = &self.cluster_buffer[start..end];
            let page = Page::from_raw(raw_slice, self.super_block.page_size, self.super_block.spare_size).ok_or(
                Ps2mcError::InvalidPageSize {
                    page_size: self.super_block.page_size,
                },
            )?;
            // page.check_ecc()?; // 可选:验证 ECC 是否正确
            let dest_start = i * page_size;
            let dest_end = dest_start + page_size;
            self.cluster_data_buffer[dest_start..dest_end].copy_from_slice(page.data);
        }
        Ok(&self.cluster_data_buffer)
    }

    fn root_entry(&mut self) -> Result<Entry, Ps2mcError> {
        self.read_entry_cluster(self.super_block.rootdir_cluster)?
            .into_iter()
            .next()
            .ok_or(Ps2mcError::InvalidRootEntry)
    }

    fn find_sub_entries(&mut self, cluster: u32, length: usize) -> Result<Vec<Entry>, Ps2mcError> {
        let mut chain_start = cluster;
        let sub_entries_count = length;
        let mut entries = Vec::with_capacity(sub_entries_count);
        let mut visited = HashSet::new();
        while chain_start != FAT_CHAIN_END && entries.len() < sub_entries_count {
            if !visited.insert(chain_start) {
                return Err(Ps2mcError::FatCycle { cluster: chain_start });
            }
            let read_entries = self.read_entry_cluster(chain_start as usize)?;
            for entry in read_entries {
                if entries.len() < sub_entries_count {
                    entries.push(entry);
                }
            }
            chain_start = self.fat_value(chain_start as usize); // 读取 FAT 表获取下一个簇索引
        }
        entries.retain(|v| v.name_bytes[0] != b'.'); // 过滤掉以 '.' 开头的条目(当前目录和父目录)
        Ok(entries)
    }

    fn fat_value(&self, n: usize) -> u32 {
        let value = self.fat_clusters[n];
        if value & FAT_ALLOCATED_BIT != 0 {
            value ^ FAT_ALLOCATED_BIT
        } else {
            value
        }
    }

    /// Returns the decoded root directory entries.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// let entries = card.entries();
    /// println!("{}", entries.len());
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn entries(&self) -> &[Entry] {
        &self.entries_in_root
    }

    /// Looks up a save directory by name in the card root.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// let _entry = card.lookup_entry("BISLPM-65109SAKA_G44");
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn lookup_entry(&self, name: &str) -> Option<&Entry> {
        self.entries_in_root.iter().find(|e| e.decode_name() == name)
    }

    /// Returns all decoded entries within a save directory.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let mut card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// let entries = card.lookup_sub_entries("BISLPM-65109SAKA_G44")?;
    /// println!("{}", entries.len());
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn lookup_sub_entries(&mut self, name: &str) -> Result<Vec<Entry>, Ps2mcError> {
        let entry = self.lookup_entry(name);
        match entry {
            Some(entry) => self.find_sub_entries(entry.cluster, entry.length),
            None => Err(Ps2mcError::SaveNotFound { name: name.to_string() }),
        }
    }

    /// Exports all files from a save directory into `output_path/name`.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let mut card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// card.export_file("BISLPM-65109SAKA_G44", "./output")?;
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn export_file(&mut self, name: &str, output_path: impl AsRef<Path>) -> Result<(), Ps2mcError> {
        let entries = self.lookup_sub_entries(name)?;
        let dir = output_path.as_ref().join(name);
        create_dir_all(&dir)?;

        for entry in entries {
            if entry.is_file() {
                let file_path = dir.join(entry.decode_name());
                let mut output_file = File::create(file_path)?;

                // 流式写入,杜绝 Vec<u8> 大内存分配
                let mut chain_start = entry.cluster;
                let mut bytes_read = 0usize;
                let mut visited = HashSet::new();

                while chain_start != FAT_CHAIN_END && bytes_read < entry.length {
                    if !visited.insert(chain_start) {
                        return Err(Ps2mcError::FatCycle { cluster: chain_start });
                    }
                    // 直接使用 cluster_data_buffer 零拷贝读取
                    let cluster_bytes = self.read_cluster(chain_start as usize + self.super_block.alloc_offset)?;
                    let remaining = entry.length - bytes_read;
                    let to_copy = remaining.min(cluster_bytes.len());

                    // 将缓冲区的数据直接写出到磁盘
                    output_file.write_all(&cluster_bytes[..to_copy])?;

                    bytes_read += to_copy;
                    chain_start = self.fat_value(chain_start as usize);
                }
            }
        }
        Ok(())
    }

    /// Reads a file payload from a cluster chain.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let mut card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// let data = card.read_file(0, 0)?;
    /// assert!(data.is_empty());
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn read_file(&mut self, cluster: u32, length: usize) -> Result<Vec<u8>, Ps2mcError> {
        let mut chain_start = cluster;
        let mut data = Vec::with_capacity(length);
        let mut bytes_read = 0usize;
        let mut visited = HashSet::new();
        while chain_start != FAT_CHAIN_END && bytes_read < length {
            if !visited.insert(chain_start) {
                return Err(Ps2mcError::FatCycle { cluster: chain_start });
            }
            let cluster_bytes = self.read_cluster(chain_start as usize + self.super_block.alloc_offset)?;
            let remaining = length - bytes_read;
            let to_copy = remaining.min(cluster_bytes.len());
            data.extend_from_slice(&cluster_bytes[..to_copy]);
            bytes_read += to_copy;
            chain_start = self.fat_value(chain_start as usize);
        }
        Ok(data)
    }

    /// Writes a file payload back to a cluster chain.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let mut card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// card.write_file(0, 0, &[])?;
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn write_file(&mut self, cluster: u32, length: usize, data: &[u8]) -> Result<(), Ps2mcError> {
        if data.len() != length {
            return Err(Ps2mcError::InvalidWriteLength {
                expected: length,
                actual: data.len(),
            });
        }
        let mut chain_start = cluster;
        let mut bytes_written = 0usize;
        let mut visited = HashSet::new();

        while chain_start != FAT_CHAIN_END && bytes_written < length {
            if !visited.insert(chain_start) {
                return Err(Ps2mcError::FatCycle { cluster: chain_start });
            }

            let physical_cluster = chain_start as usize + self.super_block.alloc_offset;
            let remaining = length - bytes_written;
            let to_write = remaining.min(self.super_block.cluster_size);

            let chunk = &data[bytes_written..bytes_written + to_write];
            // 如果写入长度不足一个簇,write_cluster 内部会保留其余部分
            self.write_cluster(physical_cluster, chunk)?;

            bytes_written += to_write;
            chain_start = self.fat_value(chain_start as usize);
        }
        if bytes_written != length {
            return Err(Ps2mcError::UnexpectedEof {
                context: "write file cluster chain",
                expected: length,
                actual: bytes_written,
            });
        }
        Ok(())
    }

    /// 写入一个簇的数据(可能只写入前几个字节,其余保留原值)
    fn write_cluster(&mut self, physical_cluster: usize, data: &[u8]) -> Result<(), Ps2mcError> {
        let cluster_size = self.super_block.cluster_size;
        let write_len = data.len().min(cluster_size);

        // 读取原有数据
        let _ = self.read_cluster(physical_cluster)?;
        // 覆盖前 write_len 字节
        self.cluster_data_buffer[..write_len].copy_from_slice(&data[..write_len]);

        // 解决借用冲突的核心:暂时把 cluster_data_buffer 从 self 里拿出来
        // (Box<[u8]> 实现有 Default,会产生一个零开销的空切片来临时占位)
        let buffer = std::mem::take(&mut self.cluster_data_buffer);
        // 写回完整簇
        // 此时 self 完全没有被 buffer 借用,可以安全地将自己作为 &mut self 传入
        let result = self.write_cluster_data(physical_cluster, &buffer);

        // 把缓冲区放回去供后续复用
        self.cluster_data_buffer = buffer;
        result
    }

    /// 将数据部分写入物理簇,重新计算并写入 ECC
    fn write_cluster_data(&mut self, physical_cluster: usize, data: &[u8]) -> Result<(), Ps2mcError> {
        let raw_page_size = self.super_block.raw_page_size;
        let page_size = self.super_block.page_size;
        let pages_per_cluster = self.super_block.pages_per_cluster;
        assert_eq!(data.len(), page_size * pages_per_cluster, "簇数据必须填满整个簇");

        let cluster_offset = (physical_cluster * pages_per_cluster * raw_page_size) as u64;

        for i in 0..pages_per_cluster {
            let page_data = &data[i * page_size..(i + 1) * page_size];
            self.page_buffer[..page_size].copy_from_slice(page_data);

            // 分块计算 ECC (512字节分成 4 个 128字节的块)
            let spare_start = page_size;
            let ecc_bytes_needed = 3 * (page_size / 128);
            for (chunk_idx, chunk) in page_data.chunks_exact(128).enumerate() {
                let ecc = calculate(chunk)?;
                // Spare 布局是:每 3 个字节为一组(3 字节 ECC)
                let offset = spare_start + chunk_idx * 3;
                self.page_buffer[offset..offset + 3].copy_from_slice(&ecc);
            }
            // 多余 spare 字节填 0
            for b in self.page_buffer[spare_start + ecc_bytes_needed..].iter_mut() {
                *b = 0;
            }

            let page_offset = cluster_offset + (i * raw_page_size) as u64;
            self.file.seek(SeekFrom::Start(page_offset))?;
            self.file.write_all(&self.page_buffer)?;
        }
        Ok(())
    }

    /// Loads the parsed icon assets for a save directory.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let mut card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// let icon_set = card.get_icon("BISLPM-65109SAKA_G44")?;
    /// println!("{}", icon_set.icons.len());
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn get_icon(&mut self, name: &str) -> Result<SaveIconSet, Ps2mcError> {
        let entries = self.lookup_sub_entries(name)?;
        let icon_sys_entry =
            entries
                .iter()
                .find(|e| e.decode_name() == "icon.sys")
                .ok_or(Ps2mcError::IconFileNotFound {
                    name: "icon.sys".to_string(),
                })?;

        let icon_sys_data = self.read_file(icon_sys_entry.cluster, icon_sys_entry.length)?;
        let icon_sys = IconSys::read_from(&icon_sys_data)?;

        let icon_names = [
            icon_sys.icon_file_normal.as_str(),
            icon_sys.icon_file_copy.as_str(),
            icon_sys.icon_file_delete.as_str(),
        ];

        let mut seen = Vec::with_capacity(3);
        let mut icons = Vec::with_capacity(3);

        for icon_name in icon_names {
            if icon_name.is_empty() || seen.contains(&icon_name) {
                continue;
            }
            seen.push(icon_name);

            let icon_entry =
                entries
                    .iter()
                    .find(|e| e.decode_name() == icon_name)
                    .ok_or(Ps2mcError::IconFileNotFound {
                        name: icon_name.to_string(),
                    })?;

            let icon_data = self.read_file(icon_entry.cluster, icon_entry.length)?;
            icons.push(Icon::parse(&icon_data)?);
        }

        Ok(SaveIconSet { icon_sys, icons })
    }

    /// Prints a summary of the decoded super block for debugging.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// let card = ps2mc::MemoryCard::open("memcard.ps2")?;
    /// card.print_info();
    /// # Ok::<(), ps2mc::Ps2mcError>(())
    /// ```
    pub fn print_info(&self) {
        let super_block = &self.super_block;
        println!("SuperBlock Info:");
        println!("Page Size: {}", super_block.page_size);
        println!("Pages per Cluster: {}", super_block.pages_per_cluster);
        println!("Clusters per Card: {}", super_block.clusters_per_card);
        println!("Allocation Offset: {}", super_block.alloc_offset);
        println!("Root Directory Cluster: {}", super_block.rootdir_cluster);
        println!("FAT per Cluster: {}", super_block.fat_per_cluster);
        println!("IFC List: {:?}", super_block.ifc_list);
        // println!("FAT Clusters: {:?}", self.fat_clusters);
    }
}