VirtualFileSystem.cs
1 using LibHac; 2 using LibHac.Common; 3 using LibHac.Common.Keys; 4 using LibHac.Fs; 5 using LibHac.Fs.Fsa; 6 using LibHac.Fs.Shim; 7 using LibHac.FsSrv; 8 using LibHac.FsSystem; 9 using LibHac.Ncm; 10 using LibHac.Sdmmc; 11 using LibHac.Spl; 12 using LibHac.Tools.Es; 13 using LibHac.Tools.Fs; 14 using LibHac.Tools.FsSystem; 15 using Ryujinx.Common.Configuration; 16 using Ryujinx.Common.Logging; 17 using Ryujinx.HLE.HOS; 18 using System; 19 using System.Buffers.Text; 20 using System.Collections.Concurrent; 21 using System.Collections.Generic; 22 using System.IO; 23 using System.Runtime.CompilerServices; 24 using Path = System.IO.Path; 25 26 namespace Ryujinx.HLE.FileSystem 27 { 28 public class VirtualFileSystem : IDisposable 29 { 30 public static readonly string SafeNandPath = Path.Combine(AppDataManager.DefaultNandDir, "safe"); 31 public static readonly string SystemNandPath = Path.Combine(AppDataManager.DefaultNandDir, "system"); 32 public static readonly string UserNandPath = Path.Combine(AppDataManager.DefaultNandDir, "user"); 33 34 public KeySet KeySet { get; private set; } 35 public EmulatedGameCard GameCard { get; private set; } 36 public SdmmcApi SdCard { get; private set; } 37 public ModLoader ModLoader { get; private set; } 38 39 private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid; 40 41 private static bool _isInitialized = false; 42 43 public static VirtualFileSystem CreateInstance() 44 { 45 if (_isInitialized) 46 { 47 throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!"); 48 } 49 50 _isInitialized = true; 51 52 return new VirtualFileSystem(); 53 } 54 55 private VirtualFileSystem() 56 { 57 ReloadKeySet(); 58 ModLoader = new ModLoader(); // Should only be created once 59 _romFsByPid = new ConcurrentDictionary<ulong, Stream>(); 60 } 61 62 public void LoadRomFs(ulong pid, string fileName) 63 { 64 var romfsStream = new FileStream(fileName, FileMode.Open, FileAccess.Read); 65 66 _romFsByPid.AddOrUpdate(pid, romfsStream, (pid, oldStream) => 67 { 68 oldStream.Close(); 69 70 return romfsStream; 71 }); 72 } 73 74 public void SetRomFs(ulong pid, Stream romfsStream) 75 { 76 _romFsByPid.AddOrUpdate(pid, romfsStream, (pid, oldStream) => 77 { 78 oldStream.Close(); 79 80 return romfsStream; 81 }); 82 } 83 84 public Stream GetRomFs(ulong pid) 85 { 86 return _romFsByPid[pid]; 87 } 88 89 public static string GetFullPath(string basePath, string fileName) 90 { 91 if (fileName.StartsWith("//")) 92 { 93 fileName = fileName[2..]; 94 } 95 else if (fileName.StartsWith('/')) 96 { 97 fileName = fileName[1..]; 98 } 99 else 100 { 101 return null; 102 } 103 104 string fullPath = Path.GetFullPath(Path.Combine(basePath, fileName)); 105 106 if (!fullPath.StartsWith(AppDataManager.BaseDirPath)) 107 { 108 return null; 109 } 110 111 return fullPath; 112 } 113 114 internal static string GetSdCardPath() => MakeFullPath(AppDataManager.DefaultSdcardDir); 115 public static string GetNandPath() => MakeFullPath(AppDataManager.DefaultNandDir); 116 117 public static string SwitchPathToSystemPath(string switchPath) 118 { 119 string[] parts = switchPath.Split(":"); 120 121 if (parts.Length != 2) 122 { 123 return null; 124 } 125 126 return GetFullPath(MakeFullPath(parts[0]), parts[1]); 127 } 128 129 public static string SystemPathToSwitchPath(string systemPath) 130 { 131 string baseSystemPath = AppDataManager.BaseDirPath + Path.DirectorySeparatorChar; 132 133 if (systemPath.StartsWith(baseSystemPath)) 134 { 135 string rawPath = systemPath.Replace(baseSystemPath, ""); 136 int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar); 137 138 if (firstSeparatorOffset == -1) 139 { 140 return $"{rawPath}:/"; 141 } 142 143 var basePath = rawPath.AsSpan(0, firstSeparatorOffset); 144 var fileName = rawPath.AsSpan(firstSeparatorOffset + 1); 145 146 return $"{basePath}:/{fileName}"; 147 } 148 149 return null; 150 } 151 152 private static string MakeFullPath(string path, bool isDirectory = true) 153 { 154 // Handles Common Switch Content Paths 155 switch (path) 156 { 157 case ContentPath.SdCard: 158 path = AppDataManager.DefaultSdcardDir; 159 break; 160 case ContentPath.User: 161 path = UserNandPath; 162 break; 163 case ContentPath.System: 164 path = SystemNandPath; 165 break; 166 case ContentPath.SdCardContent: 167 path = Path.Combine(AppDataManager.DefaultSdcardDir, "Nintendo", "Contents"); 168 break; 169 case ContentPath.UserContent: 170 path = Path.Combine(UserNandPath, "Contents"); 171 break; 172 case ContentPath.SystemContent: 173 path = Path.Combine(SystemNandPath, "Contents"); 174 break; 175 } 176 177 string fullPath = Path.Combine(AppDataManager.BaseDirPath, path); 178 179 if (isDirectory && !Directory.Exists(fullPath)) 180 { 181 Directory.CreateDirectory(fullPath); 182 } 183 184 return fullPath; 185 } 186 187 public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient) 188 { 189 LocalFileSystem serverBaseFs = new(useUnixTimeStamps: true); 190 Result result = serverBaseFs.Initialize(AppDataManager.BaseDirPath, LocalFileSystem.PathMode.DefaultCaseSensitivity, ensurePathExists: true); 191 if (result.IsFailure()) 192 { 193 throw new HorizonResultException(result, "Error creating LocalFileSystem."); 194 } 195 196 fsServerClient = horizon.CreatePrivilegedHorizonClient(); 197 var fsServer = new FileSystemServer(fsServerClient); 198 199 RandomDataGenerator randomGenerator = Random.Shared.NextBytes; 200 201 DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer, randomGenerator); 202 203 // Use our own encrypted fs creator that doesn't actually do any encryption 204 fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator(); 205 206 GameCard = fsServerObjects.GameCard; 207 SdCard = fsServerObjects.Sdmmc; 208 209 SdCard.SetSdCardInserted(true); 210 211 var fsServerConfig = new FileSystemServerConfig 212 { 213 ExternalKeySet = KeySet.ExternalKeySet, 214 FsCreators = fsServerObjects.FsCreators, 215 StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory, 216 RandomGenerator = randomGenerator, 217 }; 218 219 FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig); 220 } 221 222 public void ReloadKeySet() 223 { 224 KeySet ??= KeySet.CreateDefaultKeySet(); 225 226 string keyFile = null; 227 string titleKeyFile = null; 228 string consoleKeyFile = null; 229 230 if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile) 231 { 232 LoadSetAtPath(AppDataManager.KeysDirPathUser); 233 } 234 235 LoadSetAtPath(AppDataManager.KeysDirPath); 236 237 void LoadSetAtPath(string basePath) 238 { 239 string localKeyFile = Path.Combine(basePath, "prod.keys"); 240 string localTitleKeyFile = Path.Combine(basePath, "title.keys"); 241 string localConsoleKeyFile = Path.Combine(basePath, "console.keys"); 242 243 if (File.Exists(localKeyFile)) 244 { 245 keyFile = localKeyFile; 246 } 247 248 if (File.Exists(localTitleKeyFile)) 249 { 250 titleKeyFile = localTitleKeyFile; 251 } 252 253 if (File.Exists(localConsoleKeyFile)) 254 { 255 consoleKeyFile = localConsoleKeyFile; 256 } 257 } 258 259 ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null); 260 } 261 262 public void ImportTickets(IFileSystem fs) 263 { 264 foreach (DirectoryEntryEx ticketEntry in fs.EnumerateEntries("/", "*.tik")) 265 { 266 using var ticketFile = new UniqueRef<IFile>(); 267 268 Result result = fs.OpenFile(ref ticketFile.Ref, ticketEntry.FullPath.ToU8Span(), OpenMode.Read); 269 270 if (result.IsSuccess()) 271 { 272 // When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle 273 // of the hashed portion (usually the first 0x200 bytes) of the file and end the read after 274 // the end of the hashed portion, so we read the ticket file using a single read. 275 byte[] ticketData = new byte[0x2C0]; 276 result = ticketFile.Get.Read(out long bytesRead, 0, ticketData); 277 278 if (result.IsFailure() || bytesRead != ticketData.Length) 279 continue; 280 281 Ticket ticket = new(new MemoryStream(ticketData)); 282 var titleKey = ticket.GetTitleKey(KeySet); 283 284 if (titleKey != null) 285 { 286 KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(titleKey)); 287 } 288 } 289 } 290 } 291 292 // Save data created before we supported extra data in directory save data will not work properly if 293 // given empty extra data. Luckily some of that extra data can be created using the data from the 294 // save data indexer, which should be enough to check access permissions for user saves. 295 // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened. 296 // Consider removing this at some point in the future when we don't need to worry about old saves. 297 public static Result FixExtraData(HorizonClient hos) 298 { 299 Result rc = GetSystemSaveList(hos, out List<ulong> systemSaveIds); 300 if (rc.IsFailure()) 301 { 302 return rc; 303 } 304 305 rc = FixUnindexedSystemSaves(hos, systemSaveIds); 306 if (rc.IsFailure()) 307 { 308 return rc; 309 } 310 311 rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.System); 312 if (rc.IsFailure()) 313 { 314 return rc; 315 } 316 317 rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.User); 318 if (rc.IsFailure()) 319 { 320 return rc; 321 } 322 323 return Result.Success; 324 } 325 326 private static Result FixExtraDataInSpaceId(HorizonClient hos, SaveDataSpaceId spaceId) 327 { 328 Span<SaveDataInfo> info = stackalloc SaveDataInfo[8]; 329 330 using var iterator = new UniqueRef<SaveDataIterator>(); 331 332 Result rc = hos.Fs.OpenSaveDataIterator(ref iterator.Ref, spaceId); 333 if (rc.IsFailure()) 334 { 335 return rc; 336 } 337 338 while (true) 339 { 340 rc = iterator.Get.ReadSaveDataInfo(out long count, info); 341 if (rc.IsFailure()) 342 { 343 return rc; 344 } 345 346 if (count == 0) 347 { 348 return Result.Success; 349 } 350 351 for (int i = 0; i < count; i++) 352 { 353 rc = FixExtraData(out bool wasFixNeeded, hos, in info[i]); 354 355 if (ResultFs.TargetNotFound.Includes(rc)) 356 { 357 // If the save wasn't found, try to create the directory for its save data ID 358 rc = CreateSaveDataDirectory(hos, in info[i]); 359 360 if (rc.IsFailure()) 361 { 362 Logger.Warning?.Print(LogClass.Application, $"Error {rc.ToStringWithName()} when creating save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space"); 363 364 // Don't bother fixing the extra data if we couldn't create the directory 365 continue; 366 } 367 368 Logger.Info?.Print(LogClass.Application, $"Recreated directory for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space"); 369 370 // Try to fix the extra data in the new directory 371 rc = FixExtraData(out wasFixNeeded, hos, in info[i]); 372 } 373 374 if (rc.IsFailure()) 375 { 376 Logger.Warning?.Print(LogClass.Application, $"Error {rc.ToStringWithName()} when fixing extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space"); 377 } 378 else if (wasFixNeeded) 379 { 380 Logger.Info?.Print(LogClass.Application, $"Fixed extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space"); 381 } 382 } 383 } 384 } 385 386 private static Result CreateSaveDataDirectory(HorizonClient hos, in SaveDataInfo info) 387 { 388 if (info.SpaceId != SaveDataSpaceId.User && info.SpaceId != SaveDataSpaceId.System) 389 { 390 return Result.Success; 391 } 392 393 const string MountName = "SaveDir"; 394 var mountNameU8 = MountName.ToU8Span(); 395 396 BisPartitionId partitionId = info.SpaceId switch 397 { 398 SaveDataSpaceId.System => BisPartitionId.System, 399 SaveDataSpaceId.User => BisPartitionId.User, 400 _ => throw new ArgumentOutOfRangeException(nameof(info), info.SpaceId, null), 401 }; 402 403 Result rc = hos.Fs.MountBis(mountNameU8, partitionId); 404 if (rc.IsFailure()) 405 { 406 return rc; 407 } 408 409 try 410 { 411 var path = $"{MountName}:/save/{info.SaveDataId:x16}".ToU8Span(); 412 413 rc = hos.Fs.GetEntryType(out _, path); 414 415 if (ResultFs.PathNotFound.Includes(rc)) 416 { 417 rc = hos.Fs.CreateDirectory(path); 418 } 419 420 return rc; 421 } 422 finally 423 { 424 hos.Fs.Unmount(mountNameU8); 425 } 426 } 427 428 // Gets a list of all the save data files or directories in the system partition. 429 private static Result GetSystemSaveList(HorizonClient hos, out List<ulong> list) 430 { 431 list = null; 432 433 var mountName = "system".ToU8Span(); 434 DirectoryHandle handle = default; 435 List<ulong> localList = new(); 436 437 try 438 { 439 Result rc = hos.Fs.MountBis(mountName, BisPartitionId.System); 440 if (rc.IsFailure()) 441 { 442 return rc; 443 } 444 445 rc = hos.Fs.OpenDirectory(out handle, "system:/save".ToU8Span(), OpenDirectoryMode.All); 446 if (rc.IsFailure()) 447 { 448 return rc; 449 } 450 451 DirectoryEntry entry = new(); 452 453 while (true) 454 { 455 rc = hos.Fs.ReadDirectory(out long readCount, SpanHelpers.AsSpan(ref entry), handle); 456 if (rc.IsFailure()) 457 { 458 return rc; 459 } 460 461 if (readCount == 0) 462 { 463 break; 464 } 465 466 if (Utf8Parser.TryParse(entry.Name, out ulong saveDataId, out int bytesRead, 'x') && bytesRead == 16 && (long)saveDataId < 0) 467 { 468 localList.Add(saveDataId); 469 } 470 } 471 472 list = localList; 473 474 return Result.Success; 475 } 476 finally 477 { 478 if (handle.IsValid) 479 { 480 hos.Fs.CloseDirectory(handle); 481 } 482 483 if (hos.Fs.IsMounted(mountName)) 484 { 485 hos.Fs.Unmount(mountName); 486 } 487 } 488 } 489 490 // Adds system save data that isn't in the save data indexer to the indexer and creates extra data for it. 491 // Only save data IDs added to SystemExtraDataFixInfo will be fixed. 492 private static Result FixUnindexedSystemSaves(HorizonClient hos, List<ulong> existingSaveIds) 493 { 494 foreach (var fixInfo in _systemExtraDataFixInfo) 495 { 496 if (!existingSaveIds.Contains(fixInfo.StaticSaveDataId)) 497 { 498 continue; 499 } 500 501 Result rc = FixSystemExtraData(out bool wasFixNeeded, hos, in fixInfo); 502 503 if (rc.IsFailure()) 504 { 505 Logger.Warning?.Print(LogClass.Application, 506 $"Error {rc.ToStringWithName()} when fixing extra data for system save data 0x{fixInfo.StaticSaveDataId:x}"); 507 } 508 else if (wasFixNeeded) 509 { 510 Logger.Info?.Print(LogClass.Application, 511 $"Tried to rebuild extra data for system save data 0x{fixInfo.StaticSaveDataId:x}"); 512 } 513 } 514 515 return Result.Success; 516 } 517 518 private static Result FixSystemExtraData(out bool wasFixNeeded, HorizonClient hos, in ExtraDataFixInfo info) 519 { 520 wasFixNeeded = true; 521 522 Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.StaticSaveDataId); 523 if (!rc.IsSuccess()) 524 { 525 if (!ResultFs.TargetNotFound.Includes(rc)) 526 { 527 return rc; 528 } 529 530 // We'll reach this point only if the save data directory exists but it's not in the save data indexer. 531 // Creating the save will add it to the indexer while leaving its existing contents intact. 532 return hos.Fs.CreateSystemSaveData(info.StaticSaveDataId, UserId.InvalidId, info.OwnerId, info.DataSize, 533 info.JournalSize, info.Flags); 534 } 535 536 if (extraData.Attribute.StaticSaveDataId != 0 && extraData.OwnerId != 0) 537 { 538 wasFixNeeded = false; 539 return Result.Success; 540 } 541 542 extraData = new SaveDataExtraData 543 { 544 Attribute = { StaticSaveDataId = info.StaticSaveDataId }, 545 OwnerId = info.OwnerId, 546 Flags = info.Flags, 547 DataSize = info.DataSize, 548 JournalSize = info.JournalSize, 549 }; 550 551 // Make a mask for writing the entire extra data 552 Unsafe.SkipInit(out SaveDataExtraData extraDataMask); 553 SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF); 554 555 return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(SaveDataSpaceId.System, info.StaticSaveDataId, 556 in extraData, in extraDataMask); 557 } 558 559 private static Result FixExtraData(out bool wasFixNeeded, HorizonClient hos, in SaveDataInfo info) 560 { 561 wasFixNeeded = true; 562 563 Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.SpaceId, info.SaveDataId); 564 if (rc.IsFailure()) 565 { 566 return rc; 567 } 568 569 // The extra data should have program ID or static save data ID set if it's valid. 570 // We only try to fix the extra data if the info from the save data indexer has a program ID or static save data ID. 571 bool canFixByProgramId = extraData.Attribute.ProgramId == ProgramId.InvalidId && 572 info.ProgramId != ProgramId.InvalidId; 573 574 bool canFixBySaveDataId = extraData.Attribute.StaticSaveDataId == 0 && info.StaticSaveDataId != 0; 575 576 bool hasEmptyOwnerId = extraData.OwnerId == 0 && info.Type != SaveDataType.System; 577 578 if (!canFixByProgramId && !canFixBySaveDataId && !hasEmptyOwnerId) 579 { 580 wasFixNeeded = false; 581 return Result.Success; 582 } 583 584 // The save data attribute struct can be completely created from the save data info. 585 extraData.Attribute.ProgramId = info.ProgramId; 586 extraData.Attribute.UserId = info.UserId; 587 extraData.Attribute.StaticSaveDataId = info.StaticSaveDataId; 588 extraData.Attribute.Type = info.Type; 589 extraData.Attribute.Rank = info.Rank; 590 extraData.Attribute.Index = info.Index; 591 592 // The rest of the extra data can't be created from the save data info. 593 // On user saves the owner ID will almost certainly be the same as the program ID. 594 if (info.Type != SaveDataType.System) 595 { 596 extraData.OwnerId = info.ProgramId.Value; 597 } 598 else 599 { 600 // Try to match the system save with one of the known saves 601 foreach (ExtraDataFixInfo fixInfo in _systemExtraDataFixInfo) 602 { 603 if (extraData.Attribute.StaticSaveDataId == fixInfo.StaticSaveDataId) 604 { 605 extraData.OwnerId = fixInfo.OwnerId; 606 extraData.Flags = fixInfo.Flags; 607 extraData.DataSize = fixInfo.DataSize; 608 extraData.JournalSize = fixInfo.JournalSize; 609 610 break; 611 } 612 } 613 } 614 615 // Make a mask for writing the entire extra data 616 Unsafe.SkipInit(out SaveDataExtraData extraDataMask); 617 SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF); 618 619 return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(info.SpaceId, info.SaveDataId, in extraData, in extraDataMask); 620 } 621 622 struct ExtraDataFixInfo 623 { 624 public ulong StaticSaveDataId; 625 public ulong OwnerId; 626 public SaveDataFlags Flags; 627 public long DataSize; 628 public long JournalSize; 629 } 630 631 private static readonly ExtraDataFixInfo[] _systemExtraDataFixInfo = 632 { 633 new ExtraDataFixInfo() 634 { 635 StaticSaveDataId = 0x8000000000000030, 636 OwnerId = 0x010000000000001F, 637 Flags = SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData, 638 DataSize = 0x10000, 639 JournalSize = 0x10000, 640 }, 641 new ExtraDataFixInfo() 642 { 643 StaticSaveDataId = 0x8000000000001040, 644 OwnerId = 0x0100000000001009, 645 Flags = SaveDataFlags.None, 646 DataSize = 0xC000, 647 JournalSize = 0xC000, 648 }, 649 }; 650 651 public void Dispose() 652 { 653 GC.SuppressFinalize(this); 654 Dispose(true); 655 } 656 657 protected virtual void Dispose(bool disposing) 658 { 659 if (disposing) 660 { 661 foreach (var stream in _romFsByPid.Values) 662 { 663 stream.Close(); 664 } 665 666 _romFsByPid.Clear(); 667 } 668 } 669 } 670 }