1 // Copyright 2009 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "DiscIO/DiscScrubber.h"
6
7 #include <algorithm>
8 #include <cinttypes>
9 #include <cstddef>
10 #include <cstdio>
11 #include <memory>
12 #include <optional>
13 #include <string>
14 #include <vector>
15
16 #include "Common/Align.h"
17 #include "Common/Assert.h"
18 #include "Common/CommonTypes.h"
19 #include "Common/File.h"
20 #include "Common/Logging/Log.h"
21
22 #include "DiscIO/DiscExtractor.h"
23 #include "DiscIO/Filesystem.h"
24 #include "DiscIO/Volume.h"
25
26 namespace DiscIO
27 {
28 DiscScrubber::DiscScrubber() = default;
29 DiscScrubber::~DiscScrubber() = default;
30
SetupScrub(const Volume * disc)31 bool DiscScrubber::SetupScrub(const Volume* disc)
32 {
33 if (!disc)
34 return false;
35 m_disc = disc;
36
37 m_file_size = m_disc->GetSize();
38
39 // Round up when diving by CLUSTER_SIZE, otherwise MarkAsUsed might write out of bounds
40 const size_t num_clusters = static_cast<size_t>((m_file_size + CLUSTER_SIZE - 1) / CLUSTER_SIZE);
41
42 // Table of free blocks
43 m_free_table.resize(num_clusters, 1);
44
45 // Fill out table of free blocks
46 const bool success = ParseDisc();
47
48 m_is_scrubbing = success;
49 return success;
50 }
51
CanBlockBeScrubbed(u64 offset) const52 bool DiscScrubber::CanBlockBeScrubbed(u64 offset) const
53 {
54 return m_is_scrubbing && m_free_table[offset / CLUSTER_SIZE];
55 }
56
MarkAsUsed(u64 offset,u64 size)57 void DiscScrubber::MarkAsUsed(u64 offset, u64 size)
58 {
59 u64 current_offset = Common::AlignDown(offset, CLUSTER_SIZE);
60 const u64 end_offset = offset + size;
61
62 DEBUG_LOG(DISCIO, "Marking 0x%016" PRIx64 " - 0x%016" PRIx64 " as used", offset, end_offset);
63
64 while (current_offset < end_offset && current_offset < m_file_size)
65 {
66 m_free_table[current_offset / CLUSTER_SIZE] = 0;
67 current_offset += CLUSTER_SIZE;
68 }
69 }
70
MarkAsUsedE(u64 partition_data_offset,u64 offset,u64 size)71 void DiscScrubber::MarkAsUsedE(u64 partition_data_offset, u64 offset, u64 size)
72 {
73 if (partition_data_offset == 0)
74 {
75 MarkAsUsed(offset, size);
76 }
77 else
78 {
79 u64 first_cluster_start = ToClusterOffset(offset) + partition_data_offset;
80
81 u64 last_cluster_end;
82 if (size == 0)
83 {
84 // Without this special case, a size of 0 can be rounded to 1 cluster instead of 0
85 last_cluster_end = first_cluster_start;
86 }
87 else
88 {
89 last_cluster_end = ToClusterOffset(offset + size - 1) + CLUSTER_SIZE + partition_data_offset;
90 }
91
92 MarkAsUsed(first_cluster_start, last_cluster_end - first_cluster_start);
93 }
94 }
95
96 // Compensate for 0x400 (SHA-1) per 0x8000 (cluster), and round to whole clusters
ToClusterOffset(u64 offset) const97 u64 DiscScrubber::ToClusterOffset(u64 offset) const
98 {
99 if (m_disc->IsEncryptedAndHashed())
100 return offset / 0x7c00 * CLUSTER_SIZE;
101 else
102 return Common::AlignDown(offset, CLUSTER_SIZE);
103 }
104
105 // Helper functions for reading the BE volume
ReadFromVolume(u64 offset,u32 & buffer,const Partition & partition)106 bool DiscScrubber::ReadFromVolume(u64 offset, u32& buffer, const Partition& partition)
107 {
108 std::optional<u32> value = m_disc->ReadSwapped<u32>(offset, partition);
109 if (value)
110 buffer = *value;
111 return value.has_value();
112 }
113
ReadFromVolume(u64 offset,u64 & buffer,const Partition & partition)114 bool DiscScrubber::ReadFromVolume(u64 offset, u64& buffer, const Partition& partition)
115 {
116 std::optional<u64> value = m_disc->ReadSwappedAndShifted(offset, partition);
117 if (value)
118 buffer = *value;
119 return value.has_value();
120 }
121
ParseDisc()122 bool DiscScrubber::ParseDisc()
123 {
124 if (m_disc->GetPartitions().empty())
125 return ParsePartitionData(PARTITION_NONE);
126
127 // Mark the header as used - it's mostly 0s anyways
128 MarkAsUsed(0, 0x50000);
129
130 for (const DiscIO::Partition& partition : m_disc->GetPartitions())
131 {
132 u32 tmd_size;
133 u64 tmd_offset;
134 u32 cert_chain_size;
135 u64 cert_chain_offset;
136 u64 h3_offset;
137 // The H3 size is always 0x18000
138
139 if (!ReadFromVolume(partition.offset + 0x2a4, tmd_size, PARTITION_NONE) ||
140 !ReadFromVolume(partition.offset + 0x2a8, tmd_offset, PARTITION_NONE) ||
141 !ReadFromVolume(partition.offset + 0x2ac, cert_chain_size, PARTITION_NONE) ||
142 !ReadFromVolume(partition.offset + 0x2b0, cert_chain_offset, PARTITION_NONE) ||
143 !ReadFromVolume(partition.offset + 0x2b4, h3_offset, PARTITION_NONE))
144 {
145 return false;
146 }
147
148 MarkAsUsed(partition.offset, 0x2c0);
149
150 MarkAsUsed(partition.offset + tmd_offset, tmd_size);
151 MarkAsUsed(partition.offset + cert_chain_offset, cert_chain_size);
152 MarkAsUsed(partition.offset + h3_offset, 0x18000);
153
154 // Parse Data! This is where the big gain is
155 if (!ParsePartitionData(partition))
156 return false;
157 }
158
159 return true;
160 }
161
162 // Operations dealing with encrypted space are done here
ParsePartitionData(const Partition & partition)163 bool DiscScrubber::ParsePartitionData(const Partition& partition)
164 {
165 const FileSystem* filesystem = m_disc->GetFileSystem(partition);
166 if (!filesystem)
167 {
168 ERROR_LOG(DISCIO, "Failed to read file system for the partition at 0x%" PRIx64,
169 partition.offset);
170 return false;
171 }
172
173 u64 partition_data_offset;
174 if (partition == PARTITION_NONE)
175 {
176 partition_data_offset = 0;
177 }
178 else
179 {
180 u64 data_offset;
181 if (!ReadFromVolume(partition.offset + 0x2b8, data_offset, PARTITION_NONE))
182 return false;
183
184 partition_data_offset = partition.offset + data_offset;
185 }
186
187 // Mark things as used which are not in the filesystem
188 // Header, Header Information, Apploader
189 u32 apploader_size;
190 u32 apploader_trailer_size;
191 if (!ReadFromVolume(0x2440 + 0x14, apploader_size, partition) ||
192 !ReadFromVolume(0x2440 + 0x18, apploader_trailer_size, partition))
193 {
194 return false;
195 }
196 MarkAsUsedE(partition_data_offset, 0, 0x2440 + apploader_size + apploader_trailer_size);
197
198 // DOL
199 const std::optional<u64> dol_offset = GetBootDOLOffset(*m_disc, partition);
200 if (!dol_offset)
201 return false;
202 const std::optional<u64> dol_size = GetBootDOLSize(*m_disc, partition, *dol_offset);
203 if (!dol_size)
204 return false;
205 MarkAsUsedE(partition_data_offset, *dol_offset, *dol_size);
206
207 // FST
208 const std::optional<u64> fst_offset = GetFSTOffset(*m_disc, partition);
209 const std::optional<u64> fst_size = GetFSTSize(*m_disc, partition);
210 if (!fst_offset || !fst_size)
211 return false;
212 MarkAsUsedE(partition_data_offset, *fst_offset, *fst_size);
213
214 // Go through the filesystem and mark entries as used
215 ParseFileSystemData(partition_data_offset, filesystem->GetRoot());
216
217 return true;
218 }
219
ParseFileSystemData(u64 partition_data_offset,const FileInfo & directory)220 void DiscScrubber::ParseFileSystemData(u64 partition_data_offset, const FileInfo& directory)
221 {
222 for (const DiscIO::FileInfo& file_info : directory)
223 {
224 DEBUG_LOG(DISCIO, "Scrubbing %s", file_info.GetPath().c_str());
225 if (file_info.IsDirectory())
226 ParseFileSystemData(partition_data_offset, file_info);
227 else
228 MarkAsUsedE(partition_data_offset, file_info.GetOffset(), file_info.GetSize());
229 }
230 }
231
232 } // namespace DiscIO
233