-
Notifications
You must be signed in to change notification settings - Fork 6
/
FatxFileSystem.cs
587 lines (499 loc) · 19.3 KB
/
FatxFileSystem.cs
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
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Fsp;
// TODO:
// - more invalid/deleted checks
// - better error handling
// - metadata.ini informing about deleted files/errors?
namespace XboxWinFsp
{
public class FatxFileSystem : ReadOnlyFileSystem
{
public const int kSectorSize = 0x200;
public const uint kMagicFatx = 0x58544146;
public const uint kMagicFatxBE = 0x46415458;
public const uint kMaxDirectoryEntries = 4096; // max num of entries per directory
public const uint kMaxDirectorySize = kMaxDirectoryEntries * 0x40; // 0x40 = sizeof(FAT_DIRECTORY_ENTRY)
public const int kMaxFilenameLength = 42;
public const ushort kMaxPathLength = 240;
public const uint kClusterFree = 0;
public const uint kClusterReserved = 0xfffffff0;
public const uint kClusterBad = 0xfffffff7;
public const uint kClusterMedia = 0xfffffff8;
public const uint kClusterLast = 0xffffffff;
public const uint kCluster16Reserved = 0xfff0;
public const uint kCluster16Bad = 0xfff7;
public const uint kCluster16Media = 0xfff8;
const uint kReservedChainMapEntries = 1; // first entry in chainmap is reserved (doesn't actually exist)
const uint kFatxPageSize = 0x1000; // size of a cache-page?
FAT_VOLUME_METADATA FatHeader;
bool IsBigEndian = false;
long PartitionSize = 0;
long Position = 0;
long MaxSize = 0;
long ClusterCount = 0; // is +1 of the actual count, because first cluster is kReservedChainMapEntries..
long DataAddress = 0;
uint[] ChainMap;
// The earliest CreationTime in all the file entries
DateTime CreationTime = DateTime.Now;
BinaryReader reader;
bool IsFatx32
{
get
{
return ClusterCount >= 0xFFF0;
}
}
public uint ClusterSize
{
get
{
return FatHeader.SectorsPerCluster * FatxFileSystem.kSectorSize;
}
}
public FatxFileSystem(Stream stream, string inputPath, long partitionOffset = 0, long partitionSize = 0) : base(stream, inputPath, kSectorSize)
{
Position = partitionOffset;
PartitionSize = partitionSize;
reader = new BinaryReader(stream);
}
void FatxInit()
{
if (Position == 0)
Position = Stream.Position;
if (PartitionSize == 0)
PartitionSize = Stream.Length;
FatHeader = reader.ReadStruct<FAT_VOLUME_METADATA>();
if (FatHeader.Signature == kMagicFatxBE)
{
IsBigEndian = true;
FatHeader.EndianSwap();
}
if (FatHeader.Signature != kMagicFatx)
throw new FileSystemParseException("FATX magic invalid");
switch (FatHeader.SectorsPerCluster)
{
case 0x2:
case 0x4:
case 0x8:
case 0x10:
case 0x20:
case 0x40:
case 0x80:
break;
default:
throw new FileSystemParseException($"Invalid SectorsPerCluster value {FatHeader.SectorsPerCluster}");
}
// Work out a "maximum size" of the partition
// (xlaunch.fdf can be tiny, 1MB or so, but the kernel treats it like it's 0x1000000 bytes, so that it can expand as needed)
// (so we have to do the same in order for the chainMap calculations below to make sense)
// 0x1000000 seems to be the smallest out there, so we'll use that
MaxSize = Math.Max(0x1000000, PartitionSize);
ClusterCount = (MaxSize / ClusterSize) + kReservedChainMapEntries;
// Read in the chainmap...
Stream.Position = Position + kFatxPageSize;
ChainMap = new uint[ClusterCount];
uint numFree = 0;
int entrySize = (IsFatx32 ? 4 : 2);
byte[] chainMapBytes = new byte[ClusterCount * entrySize];
Stream.Read(chainMapBytes, 0, (int)ClusterCount * entrySize);
for (uint i = 0; i < ClusterCount; i++)
{
if (IsBigEndian)
Array.Reverse(chainMapBytes, (int)(i * entrySize), entrySize);
ChainMap[i] = IsFatx32 ? BitConverter.ToUInt32(chainMapBytes, (int)(i * entrySize)) : BitConverter.ToUInt16(chainMapBytes, (int)(i * entrySize));
// Extend 16-bit end-of-chain values
// TODO: BE 16-bit kCluster16XXX values seem messed up?
if (!IsFatx32)
if ((ChainMap[i] & 0xFFF0) == 0xFFF0 || (ChainMap[i] & 0xF0FF) == 0xF0FF)
ChainMap[i] |= 0xFFFF0000;
if (ChainMap[i] == 0)
numFree++;
}
if (ChainMap[0] != kClusterMedia && ChainMap[0] != 0xFFFFF8FF) // 0xFFFFF8FF = weird BE 16-bit value after swapping/extending...
throw new FileSystemParseException($"Invalid reserved-chainmap-entry value");
// Calculate byte totals
BytesFree = numFree * ClusterSize;
BytesInUse = ((ulong)(ClusterCount - kReservedChainMapEntries) * ClusterSize) - BytesFree;
// Calculate address of data start
long chainMapLength = ClusterCount * (IsFatx32 ? 4 : 2);
chainMapLength = (long)Utility.RoundToPages((ulong)chainMapLength, kFatxPageSize) * kFatxPageSize;
DataAddress = Position + kFatxPageSize + chainMapLength;
// Finally, start reading in the direntries...
RootFiles = FatxReadDirectory(FatHeader.RootDirFirstCluster, null);
}
List<IFileEntry> FatxReadDirectory(uint directoryCluster, FileEntry parent)
{
var entries = new List<IFileEntry>();
// Work out a maximum number of clusters to read, in case volume has some kind of corruption
int maxClusterCount = (int)(Utility.RoundToPages(kMaxDirectorySize, ClusterSize) + 1);
var directoryChain = FatxGetClusterChain(directoryCluster, maxClusterCount);
long addrStart = FatxClusterToAddress(directoryCluster);
for (int i = 0; i < directoryChain.Length; i++)
{
var cluster = directoryChain[i];
Stream.Seek(FatxClusterToAddress(cluster), SeekOrigin.Begin);
bool noMoreEntries = false;
for (int y = 0; y < (ClusterSize / 0x40); y++)
{
var entry = new FileEntry(parent, this);
if (!entry.Read(reader))
{
noMoreEntries = true;
break;
}
if (entry.CreationTime < CreationTime)
CreationTime = entry.CreationTime;
if (!entry.DirEntry.IsDeleted)
entries.Add(entry);
}
if (noMoreEntries || entries.Count >= kMaxDirectoryEntries)
break;
}
// Go back through and read directories
foreach(var entry in entries)
{
var fileEntry = (FileEntry)entry;
if (fileEntry.IsDirectory && !fileEntry.DirEntry.IsDeleted)
try
{
fileEntry.Children = FatxReadDirectory(fileEntry.DirEntry.FirstCluster, fileEntry);
}
catch
{
fileEntry.Children = new List<IFileEntry>();
break;
}
}
entries.Sort((x, y) => x.Name.CompareTo(y.Name));
return entries;
}
uint[] FatxGetClusterChain(uint cluster, int limit = int.MaxValue)
{
var chain = new List<uint>();
while (cluster != 0xFFFFFFFF && limit > chain.Count)
{
chain.Add(cluster);
cluster = ChainMap[cluster];
}
return chain.ToArray();
}
long FatxClusterToAddress(long cluster)
{
return DataAddress + ((cluster - kReservedChainMapEntries) * ClusterSize);
}
public override Int32 Init(Object Host0)
{
if (Stream == null)
return STATUS_OPEN_FAILED;
try
{
FatxInit();
var Host = (FileSystemHost)Host0;
Host.SectorSize = kSectorSize;
Host.SectorsPerAllocationUnit = (ushort)FatHeader.SectorsPerCluster;
Host.MaxComponentLength = kMaxPathLength;
Host.FileInfoTimeout = 1000;
Host.CaseSensitiveSearch = false;
Host.CasePreservedNames = true;
Host.UnicodeOnDisk = false;
Host.PersistentAcls = false;
Host.PassQueryDirectoryPattern = false;
Host.FlushAndPurgeOnCleanup = false;
try
{
Host.VolumeCreationTime = (ulong)CreationTime.ToFileTimeUtc();
}
catch
{
Host.VolumeCreationTime = 0;
}
Host.VolumeSerialNumber = FatHeader.SerialNumber;
Host.FileSystemName = $"FATX{(IsFatx32 ? "32" : "16")}";
var volName = FatHeader.VolumeName;
if (!string.IsNullOrEmpty(volName))
VolumeLabel = volName;
return base.Init(Host0);
}
catch (FileSystemParseException)
{
return STATUS_OPEN_FAILED;
}
}
// Info about a file stored inside the FATX image
protected class FileEntry : IFileEntry
{
FatxFileSystem FileSystem;
internal FAT_DIRECTORY_ENTRY DirEntry;
uint[] ClusterChain = null;
public string Name
{
get
{
return DirEntry.FileName;
}
set
{
DirEntry.FileName = value;
}
}
public ulong Size
{
get
{
return DirEntry.FileSize;
}
set
{
DirEntry.FileSize = (uint)value;
}
}
public bool IsDirectory
{
get
{
return DirEntry.IsDirectory;
}
set
{
// TODO
//DirEntry.IsDirectory = value;
}
}
public DateTime CreationTime
{
get
{
return DirEntry.CreationTime;
}
set { throw new NotImplementedException(); }
}
public DateTime LastWriteTime
{
get
{
return DirEntry.LastWriteTime;
}
set { throw new NotImplementedException(); }
}
public DateTime LastAccessTime
{
get
{
return DirEntry.LastAccessTime;
}
set { throw new NotImplementedException(); }
}
public List<IFileEntry> Children { get; set; }
public IFileEntry Parent { get; set; }
public FileEntry(FileEntry parent, FatxFileSystem fileSystem)
{
Parent = parent;
FileSystem = fileSystem;
}
public bool Read(BinaryReader reader)
{
DirEntry = reader.ReadStruct<FAT_DIRECTORY_ENTRY>();
if (FileSystem.IsBigEndian)
DirEntry.EndianSwap();
return DirEntry.IsValid;
}
public uint ReadBytes(IntPtr buffer, ulong fileOffset, uint length)
{
if (fileOffset >= Size)
return 0;
if (fileOffset + length >= Size)
length = (uint)(Size - fileOffset);
// Lock so that two threads can't try updating chain at once...
lock(this)
if (ClusterChain == null)
ClusterChain = FileSystem.FatxGetClusterChain(DirEntry.FirstCluster);
int chainIndex = (int)(fileOffset / FileSystem.ClusterSize);
int clusterOffset = (int)(fileOffset % FileSystem.ClusterSize);
int clusterRemaining = (int)(FileSystem.ClusterSize - clusterOffset);
uint lengthRemaining = length;
byte[] bytes = new byte[FileSystem.ClusterSize];
uint transferred = 0;
while (lengthRemaining > 0)
{
int readAmt = clusterRemaining;
if ((uint)readAmt > lengthRemaining)
readAmt = (int)lengthRemaining;
var clusterNum = ClusterChain[chainIndex];
int numRead = 0;
lock (FileSystem.StreamLock)
{
FileSystem.Stream.Seek(FileSystem.FatxClusterToAddress(clusterNum) + clusterOffset, SeekOrigin.Begin);
numRead = FileSystem.Stream.Read(bytes, 0, readAmt);
}
Marshal.Copy(bytes, 0, buffer, readAmt);
transferred += (uint)numRead;
if (clusterOffset + readAmt >= FileSystem.ClusterSize)
chainIndex++;
buffer += readAmt;
clusterRemaining = (int)FileSystem.ClusterSize;
clusterOffset = 0;
lengthRemaining -= (uint)readAmt;
}
return transferred;
}
public override string ToString()
{
return $"{(IsDirectory ? "D" : "F")} {Name} 0x{Size:X}";
}
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct FAT_VOLUME_METADATA
{
public uint Signature;
public uint SerialNumber;
public uint SectorsPerCluster;
public uint RootDirFirstCluster;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] VolumeNameBytes;
// [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)]
// public byte[] OnlineData;
public bool IsValid
{
get
{
return Signature == FatxFileSystem.kMagicFatx || Signature == FatxFileSystem.kMagicFatxBE;
}
}
public string VolumeName
{
get
{
return Encoding.Unicode.GetString(VolumeNameBytes).Trim(new char[] { '\0', (char)0xff, (char)0xffff });
}
}
public void EndianSwap()
{
Signature = Signature.EndianSwap();
SerialNumber = SerialNumber.EndianSwap();
SectorsPerCluster = SectorsPerCluster.EndianSwap();
RootDirFirstCluster = RootDirFirstCluster.EndianSwap();
for (int i = 0; i < VolumeNameBytes.Length; i += 2)
Array.Reverse(VolumeNameBytes, i, 2);
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct FAT_DIRECTORY_ENTRY
{
// mostly the same values as System.IO.FileAttributes... we'll keep them here just in case
enum Attribs : byte
{
ReadOnly = 1,
Hidden = 2,
System = 4,
Directory = 0x10,
Archive = 0x20,
Device = 0x40,
Normal = 0x80
}
public byte FileNameLength;
public byte Attributes;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 42)]
public byte[] FileNameBytes;
public uint FirstCluster;
public uint FileSize;
public int CreationTimeRaw;
public int LastWriteTimeRaw;
public int LastAccessTimeRaw;
public bool IsDirectory
{
get
{
return (Attributes & (byte)Attribs.Directory) == (byte)Attribs.Directory;
}
}
bool IsValidFileNameLength
{
get
{
if (IsDeleted)
return true;
return FileNameLength > 0 && FileNameLength <= FatxFileSystem.kMaxFilenameLength;
}
}
public bool IsValid
{
get
{
try
{
if (!IsValidFileNameLength || FirstCluster <= 0)
return false;
// ToFileTimeUtc will throw exception if time is invalid
// Just hope these don't get optimized out..
bool test1 = CreationTime.ToFileTimeUtc() == CreationTime.ToFileTimeUtc();
bool test2 = LastWriteTime.ToFileTimeUtc() == LastWriteTime.ToFileTimeUtc();
bool test3 = LastAccessTime.ToFileTimeUtc() == LastAccessTime.ToFileTimeUtc();
return test1 && test2 && test3; // TODO: more checks?
}
catch
{
return false;
}
}
}
public bool IsDeleted
{
get
{
return FileNameLength == 0xE5;
}
}
public DateTime CreationTime
{
get
{
return Utility.DecodeMSTime(CreationTimeRaw);
}
}
public DateTime LastWriteTime
{
get
{
return Utility.DecodeMSTime(LastWriteTimeRaw);
}
}
public DateTime LastAccessTime
{
get
{
return Utility.DecodeMSTime(LastAccessTimeRaw);
}
}
public string FileName
{
get
{
if (IsDeleted)
return "_" + Encoding.ASCII.GetString(FileNameBytes).Trim(new char[] { '\0', (char)0xff });
else
return Encoding.ASCII.GetString(FileNameBytes, 0, FileNameLength).Trim(new char[] { '\0', (char)0xff });
}
set
{
if(!IsDeleted)
FileNameLength = (byte)value.Length;
FileNameBytes = Encoding.ASCII.GetBytes(value);
if (FileNameBytes.Length > FatxFileSystem.kMaxFilenameLength)
Array.Resize(ref FileNameBytes, FatxFileSystem.kMaxFilenameLength);
}
}
public void EndianSwap()
{
FirstCluster = FirstCluster.EndianSwap();
FileSize = FileSize.EndianSwap();
CreationTimeRaw = CreationTimeRaw.EndianSwap();
LastWriteTimeRaw = LastWriteTimeRaw.EndianSwap();
LastAccessTimeRaw = LastAccessTimeRaw.EndianSwap();
}
}
}