1 // Copyright 2020 Citra Emulator Project
2 // Licensed under GPLv2 or any later version
3 // Refer to the license.txt file included.
4 
5 #include <algorithm>
6 #include <cstring>
7 #include "common/alignment.h"
8 #include "common/archives.h"
9 #include "common/assert.h"
10 #include "common/common_paths.h"
11 #include "common/file_util.h"
12 #include "common/string_util.h"
13 #include "common/swap.h"
14 #include "core/file_sys/layered_fs.h"
15 #include "core/file_sys/patch.h"
16 
17 SERIALIZE_EXPORT_IMPL(FileSys::LayeredFS)
18 
19 namespace FileSys {
20 
21 struct FileRelocationInfo {
22     int type;                      // 0 - none, 1 - replaced / created, 2 - patched, 3 - removed
23     u64 original_offset;           // Type 0. Offset is absolute
24     std::string replace_file_path; // Type 1
25     std::vector<u8> patched_file;  // Type 2
26     u64 size;                      // Relocated file size
27 };
28 struct LayeredFS::File {
29     std::string name;
30     std::string path;
31     FileRelocationInfo relocation{};
32     Directory* parent;
33 };
34 
35 struct DirectoryMetadata {
36     u32_le parent_directory_offset;
37     u32_le next_sibling_offset;
38     u32_le first_child_directory_offset;
39     u32_le first_file_offset;
40     u32_le hash_bucket_next;
41     u32_le name_length;
42     // Followed by a name of name length (aligned up to 4)
43 };
44 static_assert(sizeof(DirectoryMetadata) == 0x18, "Size of DirectoryMetadata is not correct");
45 
46 struct FileMetadata {
47     u32_le parent_directory_offset;
48     u32_le next_sibling_offset;
49     u64_le file_data_offset;
50     u64_le file_data_length;
51     u32_le hash_bucket_next;
52     u32_le name_length;
53     // Followed by a name of name length (aligned up to 4)
54 };
55 static_assert(sizeof(FileMetadata) == 0x20, "Size of FileMetadata is not correct");
56 
57 LayeredFS::LayeredFS() = default;
58 
LayeredFS(std::shared_ptr<RomFSReader> romfs_,std::string patch_path_,std::string patch_ext_path_,bool load_relocations_)59 LayeredFS::LayeredFS(std::shared_ptr<RomFSReader> romfs_, std::string patch_path_,
60                      std::string patch_ext_path_, bool load_relocations_)
61     : romfs(std::move(romfs_)), patch_path(std::move(patch_path_)),
62       patch_ext_path(std::move(patch_ext_path_)), load_relocations(load_relocations_) {
63     Load();
64 }
65 
Load()66 void LayeredFS::Load() {
67     romfs->ReadFile(0, sizeof(header), reinterpret_cast<u8*>(&header));
68 
69     ASSERT_MSG(header.header_length == sizeof(header), "Header size is incorrect");
70 
71     // TODO: is root always the first directory in table?
72     root.parent = &root;
73     LoadDirectory(root, 0);
74 
75     if (load_relocations) {
76         LoadRelocations();
77         LoadExtRelocations();
78     }
79 
80     RebuildMetadata();
81 }
82 
83 LayeredFS::~LayeredFS() = default;
84 
LoadDirectory(Directory & current,u32 offset)85 u32 LayeredFS::LoadDirectory(Directory& current, u32 offset) {
86     DirectoryMetadata metadata;
87     romfs->ReadFile(header.directory_metadata_table.offset + offset, sizeof(metadata),
88                     reinterpret_cast<u8*>(&metadata));
89 
90     current.name = ReadName(header.directory_metadata_table.offset + offset + sizeof(metadata),
91                             metadata.name_length);
92     current.path = current.parent->path + current.name + DIR_SEP;
93     directory_path_map.emplace(current.path, &current);
94 
95     u32 file_offset = metadata.first_file_offset;
96     while (file_offset != 0xFFFFFFFF) {
97         file_offset = LoadFile(current, file_offset);
98     }
99 
100     u32 child_directory_offset = metadata.first_child_directory_offset;
101     while (child_directory_offset != 0xFFFFFFFF) {
102         auto child = std::make_unique<Directory>();
103         auto& directory = *child;
104         directory.parent = &current;
105         current.directories.emplace_back(std::move(child));
106         child_directory_offset = LoadDirectory(directory, child_directory_offset);
107     }
108 
109     return metadata.next_sibling_offset;
110 }
111 
LoadFile(Directory & parent,u32 offset)112 u32 LayeredFS::LoadFile(Directory& parent, u32 offset) {
113     FileMetadata metadata;
114     romfs->ReadFile(header.file_metadata_table.offset + offset, sizeof(metadata),
115                     reinterpret_cast<u8*>(&metadata));
116 
117     auto file = std::make_unique<File>();
118     file->name = ReadName(header.file_metadata_table.offset + offset + sizeof(metadata),
119                           metadata.name_length);
120     file->path = parent.path + file->name;
121     file->relocation.original_offset = header.file_data_offset + metadata.file_data_offset;
122     file->relocation.size = metadata.file_data_length;
123     file->parent = &parent;
124 
125     file_path_map.emplace(file->path, file.get());
126     parent.files.emplace_back(std::move(file));
127 
128     return metadata.next_sibling_offset;
129 }
130 
ReadName(u32 offset,u32 name_length)131 std::string LayeredFS::ReadName(u32 offset, u32 name_length) {
132     std::vector<u16_le> buffer(name_length / sizeof(u16_le));
133     romfs->ReadFile(offset, name_length, reinterpret_cast<u8*>(buffer.data()));
134 
135     std::u16string name(buffer.size(), 0);
136     std::transform(buffer.begin(), buffer.end(), name.begin(), [](u16_le character) {
137         return static_cast<char16_t>(static_cast<u16>(character));
138     });
139     return Common::UTF16ToUTF8(name);
140 }
141 
LoadRelocations()142 void LayeredFS::LoadRelocations() {
143     if (!FileUtil::Exists(patch_path)) {
144         return;
145     }
146 
147     const FileUtil::DirectoryEntryCallable callback = [this,
148                                                        &callback](u64* /*num_entries_out*/,
149                                                                   const std::string& directory,
150                                                                   const std::string& virtual_name) {
151         auto* parent = directory_path_map.at(directory.substr(patch_path.size() - 1));
152 
153         if (FileUtil::IsDirectory(directory + virtual_name + DIR_SEP)) {
154             const auto path = (directory + virtual_name + DIR_SEP).substr(patch_path.size() - 1);
155             if (!directory_path_map.count(path)) { // Add this directory
156                 auto directory = std::make_unique<Directory>();
157                 directory->name = virtual_name;
158                 directory->path = path;
159                 directory->parent = parent;
160                 directory_path_map.emplace(path, directory.get());
161                 parent->directories.emplace_back(std::move(directory));
162                 LOG_INFO(Service_FS, "LayeredFS created directory {}", path);
163             }
164             return FileUtil::ForeachDirectoryEntry(nullptr, directory + virtual_name + DIR_SEP,
165                                                    callback);
166         }
167 
168         const auto path = (directory + virtual_name).substr(patch_path.size() - 1);
169         if (!file_path_map.count(path)) { // Newly created file
170             auto file = std::make_unique<File>();
171             file->name = virtual_name;
172             file->path = path;
173             file->parent = parent;
174             file_path_map.emplace(path, file.get());
175             parent->files.emplace_back(std::move(file));
176             LOG_INFO(Service_FS, "LayeredFS created file {}", path);
177         }
178 
179         auto* file = file_path_map.at(path);
180         file->relocation.type = 1;
181         file->relocation.replace_file_path = directory + virtual_name;
182         file->relocation.size = FileUtil::GetSize(directory + virtual_name);
183         LOG_INFO(Service_FS, "LayeredFS replacement file in use for {}", path);
184         return true;
185     };
186 
187     FileUtil::ForeachDirectoryEntry(nullptr, patch_path, callback);
188 }
189 
LoadExtRelocations()190 void LayeredFS::LoadExtRelocations() {
191     if (!FileUtil::Exists(patch_ext_path)) {
192         return;
193     }
194 
195     if (patch_ext_path.back() == '/' || patch_ext_path.back() == '\\') {
196         // ScanDirectoryTree expects a path without trailing '/'
197         patch_ext_path.erase(patch_ext_path.size() - 1, 1);
198     }
199 
200     FileUtil::FSTEntry result;
201     FileUtil::ScanDirectoryTree(patch_ext_path, result, 256);
202 
203     for (const auto& entry : result.children) {
204         if (FileUtil::IsDirectory(entry.physicalName)) {
205             continue;
206         }
207 
208         const auto path = entry.physicalName.substr(patch_ext_path.size());
209         if (path.size() >= 5 && path.substr(path.size() - 5) == ".stub") {
210             // Remove the corresponding file if exists
211             const auto file_path = path.substr(0, path.size() - 5);
212             if (file_path_map.count(file_path)) {
213                 auto& file = *file_path_map[file_path];
214                 file.relocation.type = 3;
215                 file.relocation.size = 0;
216                 file_path_map.erase(file_path);
217                 LOG_INFO(Service_FS, "LayeredFS removed file {}", file_path);
218             } else {
219                 LOG_WARNING(Service_FS, "LayeredFS file for stub {} not found", path);
220             }
221         } else if (path.size() >= 4) {
222             const auto extension = path.substr(path.size() - 4);
223             if (extension != ".ips" && extension != ".bps") {
224                 LOG_WARNING(Service_FS, "LayeredFS unknown ext file {}", path);
225             }
226 
227             const auto file_path = path.substr(0, path.size() - 4);
228             if (!file_path_map.count(file_path)) {
229                 LOG_WARNING(Service_FS, "LayeredFS original file for patch {} not found", path);
230                 continue;
231             }
232 
233             FileUtil::IOFile patch_file(entry.physicalName, "rb");
234             if (!patch_file) {
235                 LOG_ERROR(Service_FS, "LayeredFS Could not open file {}", entry.physicalName);
236                 continue;
237             }
238 
239             const auto size = patch_file.GetSize();
240             std::vector<u8> patch(size);
241             if (patch_file.ReadBytes(patch.data(), size) != size) {
242                 LOG_ERROR(Service_FS, "LayeredFS Could not read file {}", entry.physicalName);
243                 continue;
244             }
245 
246             auto& file = *file_path_map[file_path];
247             std::vector<u8> buffer(file.relocation.size); // Original size
248             romfs->ReadFile(file.relocation.original_offset, buffer.size(), buffer.data());
249 
250             bool ret = false;
251             if (extension == ".ips") {
252                 ret = Patch::ApplyIpsPatch(patch, buffer);
253             } else {
254                 ret = Patch::ApplyBpsPatch(patch, buffer);
255             }
256 
257             if (ret) {
258                 LOG_INFO(Service_FS, "LayeredFS patched file {}", file_path);
259 
260                 file.relocation.type = 2;
261                 file.relocation.size = buffer.size();
262                 file.relocation.patched_file = std::move(buffer);
263             } else {
264                 LOG_ERROR(Service_FS, "LayeredFS failed to patch file {}", file_path);
265             }
266         } else {
267             LOG_WARNING(Service_FS, "LayeredFS unknown ext file {}", path);
268         }
269     }
270 }
271 
GetNameSize(const std::string & name)272 static std::size_t GetNameSize(const std::string& name) {
273     std::u16string u16name = Common::UTF8ToUTF16(name);
274     return Common::AlignUp(u16name.size() * 2, 4);
275 }
276 
PrepareBuildDirectory(Directory & current)277 void LayeredFS::PrepareBuildDirectory(Directory& current) {
278     directory_metadata_offset_map.emplace(&current, static_cast<u32>(current_directory_offset));
279     directory_list.emplace_back(&current);
280     current_directory_offset += sizeof(DirectoryMetadata) + GetNameSize(current.name);
281 }
282 
PrepareBuildFile(File & current)283 void LayeredFS::PrepareBuildFile(File& current) {
284     if (current.relocation.type == 3) { // Deleted files are not counted
285         return;
286     }
287     file_metadata_offset_map.emplace(&current, static_cast<u32>(current_file_offset));
288     file_list.emplace_back(&current);
289     current_file_offset += sizeof(FileMetadata) + GetNameSize(current.name);
290 }
291 
PrepareBuild(Directory & current)292 void LayeredFS::PrepareBuild(Directory& current) {
293     for (const auto& child : current.files) {
294         PrepareBuildFile(*child);
295     }
296 
297     for (const auto& child : current.directories) {
298         PrepareBuildDirectory(*child);
299     }
300 
301     for (const auto& child : current.directories) {
302         PrepareBuild(*child);
303     }
304 }
305 
306 // Implementation from 3dbrew
CalcHash(const std::string & name,u32 parent_offset)307 static u32 CalcHash(const std::string& name, u32 parent_offset) {
308     u32 hash = parent_offset ^ 123456789;
309     std::u16string u16name = Common::UTF8ToUTF16(name);
310     for (char16_t c : u16name) {
311         hash = (hash >> 5) | (hash << 27);
312         hash ^= static_cast<u16>(c);
313     }
314     return hash;
315 }
316 
WriteName(u8 * dest,std::u16string name)317 static std::size_t WriteName(u8* dest, std::u16string name) {
318     const auto buffer_size = Common::AlignUp(name.size() * 2, 4);
319     std::vector<u16_le> buffer(buffer_size / 2);
320     std::transform(name.begin(), name.end(), buffer.begin(), [](char16_t character) {
321         return static_cast<u16_le>(static_cast<u16>(character));
322     });
323     std::memcpy(dest, buffer.data(), buffer_size);
324 
325     return buffer_size;
326 }
327 
BuildDirectories()328 void LayeredFS::BuildDirectories() {
329     directory_metadata_table.resize(current_directory_offset, 0xFF);
330 
331     std::size_t written = 0;
332     for (const auto& directory : directory_list) {
333         DirectoryMetadata metadata;
334         std::memset(&metadata, 0xFF, sizeof(metadata));
335         metadata.parent_directory_offset = directory_metadata_offset_map.at(directory->parent);
336 
337         if (directory->parent != directory) {
338             bool flag = false;
339             for (const auto& sibling : directory->parent->directories) {
340                 if (flag) {
341                     metadata.next_sibling_offset = directory_metadata_offset_map.at(sibling.get());
342                     break;
343                 } else if (sibling.get() == directory) {
344                     flag = true;
345                 }
346             }
347         }
348 
349         if (!directory->directories.empty()) {
350             metadata.first_child_directory_offset =
351                 directory_metadata_offset_map.at(directory->directories.front().get());
352         }
353 
354         if (!directory->files.empty()) {
355             metadata.first_file_offset =
356                 file_metadata_offset_map.at(directory->files.front().get());
357         }
358 
359         const auto bucket = CalcHash(directory->name, metadata.parent_directory_offset) %
360                             directory_hash_table.size();
361         metadata.hash_bucket_next = directory_hash_table[bucket];
362         directory_hash_table[bucket] = directory_metadata_offset_map.at(directory);
363 
364         // Write metadata and name
365         std::u16string u16name = Common::UTF8ToUTF16(directory->name);
366         metadata.name_length = static_cast<u32_le>(u16name.size() * 2);
367 
368         std::memcpy(directory_metadata_table.data() + written, &metadata, sizeof(metadata));
369         written += sizeof(metadata);
370 
371         written += WriteName(directory_metadata_table.data() + written, u16name);
372     }
373 
374     ASSERT_MSG(written == directory_metadata_table.size(),
375                "Calculated size for directory metadata table is wrong");
376 }
377 
BuildFiles()378 void LayeredFS::BuildFiles() {
379     file_metadata_table.resize(current_file_offset, 0xFF);
380 
381     std::size_t written = 0;
382     for (const auto& file : file_list) {
383         FileMetadata metadata;
384         std::memset(&metadata, 0xFF, sizeof(metadata));
385 
386         metadata.parent_directory_offset = directory_metadata_offset_map.at(file->parent);
387 
388         bool flag = false;
389         for (const auto& sibling : file->parent->files) {
390             if (sibling->relocation.type == 3) { // removed file
391                 continue;
392             }
393             if (flag) {
394                 metadata.next_sibling_offset = file_metadata_offset_map.at(sibling.get());
395                 break;
396             } else if (sibling.get() == file) {
397                 flag = true;
398             }
399         }
400 
401         metadata.file_data_offset = current_data_offset;
402         metadata.file_data_length = file->relocation.size;
403         current_data_offset += Common::AlignUp(metadata.file_data_length, 16);
404         if (metadata.file_data_length != 0) {
405             data_offset_map.emplace(metadata.file_data_offset, file);
406         }
407 
408         const auto bucket =
409             CalcHash(file->name, metadata.parent_directory_offset) % file_hash_table.size();
410         metadata.hash_bucket_next = file_hash_table[bucket];
411         file_hash_table[bucket] = file_metadata_offset_map.at(file);
412 
413         // Write metadata and name
414         std::u16string u16name = Common::UTF8ToUTF16(file->name);
415         metadata.name_length = static_cast<u32_le>(u16name.size() * 2);
416 
417         std::memcpy(file_metadata_table.data() + written, &metadata, sizeof(metadata));
418         written += sizeof(metadata);
419 
420         written += WriteName(file_metadata_table.data() + written, u16name);
421     }
422 
423     ASSERT_MSG(written == file_metadata_table.size(),
424                "Calculated size for file metadata table is wrong");
425 }
426 
427 // Implementation from 3dbrew
GetHashTableSize(std::size_t entry_count)428 static std::size_t GetHashTableSize(std::size_t entry_count) {
429     if (entry_count < 3) {
430         return 3;
431     } else if (entry_count < 19) {
432         return entry_count | 1;
433     } else {
434         std::size_t count = entry_count;
435         while (count % 2 == 0 || count % 3 == 0 || count % 5 == 0 || count % 7 == 0 ||
436                count % 11 == 0 || count % 13 == 0 || count % 17 == 0) {
437             count++;
438         }
439         return count;
440     }
441 }
442 
RebuildMetadata()443 void LayeredFS::RebuildMetadata() {
444     PrepareBuildDirectory(root);
445     PrepareBuild(root);
446 
447     directory_hash_table.resize(GetHashTableSize(directory_list.size()), 0xFFFFFFFF);
448     file_hash_table.resize(GetHashTableSize(file_list.size()), 0xFFFFFFFF);
449 
450     BuildDirectories();
451     BuildFiles();
452 
453     // Create header
454     RomFSHeader header;
455     header.header_length = sizeof(header);
456     header.directory_hash_table = {
457         /*offset*/ sizeof(header),
458         /*length*/ static_cast<u32_le>(directory_hash_table.size() * sizeof(u32_le))};
459     header.directory_metadata_table = {
460         /*offset*/
461         header.directory_hash_table.offset + header.directory_hash_table.length,
462         /*length*/ static_cast<u32_le>(directory_metadata_table.size())};
463     header.file_hash_table = {
464         /*offset*/
465         header.directory_metadata_table.offset + header.directory_metadata_table.length,
466         /*length*/ static_cast<u32_le>(file_hash_table.size() * sizeof(u32_le))};
467     header.file_metadata_table = {/*offset*/ header.file_hash_table.offset +
468                                       header.file_hash_table.length,
469                                   /*length*/ static_cast<u32_le>(file_metadata_table.size())};
470     header.file_data_offset =
471         Common::AlignUp(header.file_metadata_table.offset + header.file_metadata_table.length, 16);
472 
473     // Write hash table and metadata table
474     metadata.resize(header.file_data_offset);
475     std::memcpy(metadata.data(), &header, header.header_length);
476     std::memcpy(metadata.data() + header.directory_hash_table.offset, directory_hash_table.data(),
477                 header.directory_hash_table.length);
478     std::memcpy(metadata.data() + header.directory_metadata_table.offset,
479                 directory_metadata_table.data(), header.directory_metadata_table.length);
480     std::memcpy(metadata.data() + header.file_hash_table.offset, file_hash_table.data(),
481                 header.file_hash_table.length);
482     std::memcpy(metadata.data() + header.file_metadata_table.offset, file_metadata_table.data(),
483                 header.file_metadata_table.length);
484 }
485 
GetSize() const486 std::size_t LayeredFS::GetSize() const {
487     return metadata.size() + current_data_offset;
488 }
489 
ReadFile(std::size_t offset,std::size_t length,u8 * buffer)490 std::size_t LayeredFS::ReadFile(std::size_t offset, std::size_t length, u8* buffer) {
491     ASSERT_MSG(offset + length <= GetSize(), "Out of bound");
492 
493     std::size_t read_size = 0;
494     if (offset < metadata.size()) {
495         // First read the metadata
496         const auto to_read = std::min(metadata.size() - offset, length);
497         std::memcpy(buffer, metadata.data() + offset, to_read);
498         read_size += to_read;
499         offset = 0;
500     } else {
501         offset -= metadata.size();
502     }
503 
504     // Read files
505     auto current = (--data_offset_map.upper_bound(offset));
506     while (read_size < length) {
507         const auto relative_offset = offset - current->first;
508         std::size_t to_read{};
509         if (current->second->relocation.size > relative_offset) {
510             to_read = std::min<std::size_t>(current->second->relocation.size - relative_offset,
511                                             length - read_size);
512         }
513         const auto alignment =
514             std::min<std::size_t>(Common::AlignUp(current->second->relocation.size, 16) -
515                                       relative_offset,
516                                   length - read_size) -
517             to_read;
518 
519         // Read the file in different ways depending on relocation type
520         auto& relocation = current->second->relocation;
521         if (relocation.type == 0) { // none
522             romfs->ReadFile(relocation.original_offset + relative_offset, to_read,
523                             buffer + read_size);
524         } else if (relocation.type == 1) { // replace
525             FileUtil::IOFile replace_file(relocation.replace_file_path, "rb");
526             if (replace_file) {
527                 replace_file.Seek(relative_offset, SEEK_SET);
528                 replace_file.ReadBytes(buffer + read_size, to_read);
529             } else {
530                 LOG_ERROR(Service_FS, "Could not open replacement file for {}",
531                           current->second->path);
532             }
533         } else if (relocation.type == 2) { // patch
534             std::memcpy(buffer + read_size, relocation.patched_file.data() + relative_offset,
535                         to_read);
536         } else {
537             UNREACHABLE();
538         }
539 
540         std::memset(buffer + read_size + to_read, 0, alignment);
541 
542         read_size += to_read + alignment;
543         offset += to_read + alignment;
544         current++;
545     }
546 
547     return read_size;
548 }
549 
ExtractDirectory(Directory & current,const std::string & target_path)550 bool LayeredFS::ExtractDirectory(Directory& current, const std::string& target_path) {
551     if (!FileUtil::CreateFullPath(target_path + current.path)) {
552         LOG_ERROR(Service_FS, "Could not create path {}", target_path + current.path);
553         return false;
554     }
555 
556     constexpr std::size_t BufferSize = 0x10000;
557     std::array<u8, BufferSize> buffer;
558     for (const auto& file : current.files) {
559         // Extract file
560         const auto path = target_path + file->path;
561         LOG_INFO(Service_FS, "Extracting {} to {}", file->path, path);
562 
563         FileUtil::IOFile target_file(path, "wb");
564         if (!target_file) {
565             LOG_ERROR(Service_FS, "Could not open file {}", path);
566             return false;
567         }
568 
569         std::size_t written = 0;
570         while (written < file->relocation.size) {
571             const auto to_read =
572                 std::min<std::size_t>(buffer.size(), file->relocation.size - written);
573             if (romfs->ReadFile(file->relocation.original_offset + written, to_read,
574                                 buffer.data()) != to_read) {
575                 LOG_ERROR(Service_FS, "Could not read from RomFS");
576                 return false;
577             }
578 
579             if (target_file.WriteBytes(buffer.data(), to_read) != to_read) {
580                 LOG_ERROR(Service_FS, "Could not write to file {}", path);
581                 return false;
582             }
583 
584             written += to_read;
585         }
586     }
587 
588     for (const auto& directory : current.directories) {
589         if (!ExtractDirectory(*directory, target_path)) {
590             return false;
591         }
592     }
593 
594     return true;
595 }
596 
DumpRomFS(const std::string & target_path)597 bool LayeredFS::DumpRomFS(const std::string& target_path) {
598     std::string path = target_path;
599     if (path.back() == '/' || path.back() == '\\') {
600         path.erase(path.size() - 1, 1);
601     }
602 
603     return ExtractDirectory(root, path);
604 }
605 
606 } // namespace FileSys
607