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
// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>
use crate::error::Error;
use crate::metadata::Metadata;
use std::cmp;
use std::io;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use ctr::cipher::StreamCipher;
use ctr::cipher::StreamCipherSeek;
const fn is_overlap(region1: (u32, u32), region2: (u32, u32)) -> bool {
(region1.0 <= region2.1) && (region1.1 >= region2.0)
}
/// Represents a readable 3DS cartridge.
pub struct Cart<T: Read + Seek> {
/// Metadata of the cartridge.
pub metadata: Metadata,
/// Internal handle to the underlying IO object used to access the cartridge.
io: T,
}
impl<T: Read + Seek> Cart<T> {
/// Creates a [`Cart`] from an IO object.
///
/// # Errors
///
/// See [`Metadata::try_from`] for more details.
pub fn try_from(mut io: T) -> Result<Self, Error> {
Ok(Self::new(Metadata::try_from(&mut io)?, io))
}
/// Creates a new [`Cart`] from the given arguments.
pub const fn new(metadata: Metadata, io: T) -> Self {
Self { metadata, io }
}
/// Consumes the current [`Cart`] and returns its components.
#[must_use]
pub fn release(self) -> (Metadata, T) {
(self.metadata, self.io)
}
}
impl<T: Read + Seek> Read for Cart<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let cur = self.io.stream_position()?;
let cur = u32::try_from(cur).map_err(|_| io::Error::from(io::ErrorKind::Other))?;
// FIXME I don't know how I want to handle this. In reality, for the next... what, 20
// years? We won't ever have a case where usize won't unwrap into a u64
let end = cur
+ u32::try_from(buf.len()).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
let result = self.io.read(buf)?;
// Handle ExeFS descryption. However, right now we're only using the info key so we can
// read the icon from ExeFS
// FIXME: what if nocrypto flag?
let exefs_ncch = self.metadata.ncsd.find_exefs_ncch();
if let Some(exefs_ncch) = exefs_ncch {
let exefs_offset =
(exefs_ncch.partition_entry.offset + exefs_ncch.exefs_offset) * 0x200;
let exefs_region = (exefs_offset, exefs_offset + exefs_ncch.exefs_size * 0x200);
if is_overlap(exefs_region, (cur, end)) {
// Figure out the overlap, then decrypt that
let start = cmp::max(exefs_region.0, cur);
let end = cmp::min(exefs_region.1, end);
// no risk of panic, since this wouldn't be None if there's an ExeFS section
self.metadata
.exefs_ctr
.as_mut()
.unwrap()
.seek(start - exefs_region.0);
// FIXME not a fan of unwrap, but I'm so tired of dealing with usize
let start = usize::try_from(start).unwrap();
let end = usize::try_from(end).unwrap();
let cur = usize::try_from(cur).unwrap();
let start = start - cur;
let end = end - cur;
// no risk of panic, since this wouldn't be None if there's an ExeFS section
self.metadata
.exefs_ctr
.as_mut()
.unwrap()
.apply_keystream(&mut buf[start..end]);
}
}
Ok(result)
}
}
impl<T: Read + Seek> Seek for Cart<T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.io.seek(pos)
}
}