Unlike the first two installments in the trilogy, Mass Effect 3 stores its DLC content inside a compressed, proprietary archive called the SFAR. SFARs use both LZMA and LZX compression.
File Structure[]
The SFAR file is divided into four main chunks: header, file table, block size table, and blocks. This article will deal with the SFAR file specifications, for information about serializing code, consult the forum's research thread on SFAR file format [1].
Header
The Header contains information about the various chunks that comprise the file.
Bytes | Type | Description |
---|---|---|
4 | uint | Magic (0x53464152 = RAFS = SFAR reverse) |
4 | uint | Version (always 0x00010000) |
4 | uint | Data offset |
4 | uint | Entry offset (always 0x20); start of File Table |
4 | uint | File count |
4 | uint | Block-Size Table offset |
4 | uint | Max. block size (always 0x00010000) |
4 | char[ ] | Compression scheme (None = 0x6E6F6E65; LZMA = 0x6C7A6D61; LZX = 0x6C7A7820) |
File Table
The File Table is a list of file entries. The number of entries is defined in the file count field of the Header. Each entry struct is 0x1E (30) bytes long, with the following spec:
Bytes | Type | Description |
---|---|---|
4 | uint | HashA |
4 | uint | HashB |
4 | uint | HashC |
4 | uint | HashD |
4 | uint | Block-Size Table index |
4 | uint | Uncompressed size |
4 | byte | Uncompressed size Adder |
4 | uint | Data offset |
4 | byte | Data offset Adder |
Despite having the uncompressed size and data offset fields in the above table, their actual values need to be calculated as follows:
RealUncompressedSize = UncompressedSize + UncompressedSizeAdder << 32; RealDataOffset = DataOffset + DataOffsetAdder << 32;
If the field Block-Size index == -1 (0xFFFFFFFF), then this entry corresponds to exactly one block in the Blocks chunk. In which case:
Block Size == RealUncompressedSize Block Offset == RealDataOffset
Block-Size Table
Each entry in the Block-Size Table is 2 bytes long (ushort), and this value specifies the Block-Size corresponding to an entry in the File Table.
Bytes | Type | Description |
---|---|---|
2 | ushort | Block size |
An entry in the File Table refers to another entry in the Block-Size Table when its Block-Size index > -1 (0xFFFFFFFF). In which case:
Block Size == Block-Size Table value Block Offset == RealDataOffset
Blocks
The Blocks chunk contains the raw files data inside the SFAR. From the previous sections it can be inferred that this data may or may not be compressed.
Locating File Names[]
One of the entries in the File Table contains the following hash:
0xB5, 0x50, 0x19, 0xCB, 0xF9, 0xD3, 0xDA, 0x65, 0xD5, 0x5B, 0x32, 0x1C, 0x00, 0x19, 0x69, 0x7C
The content of this hash is a text string, with each file's relative path and name inside the SFAR defined in a new line. For example:
/BIOGame/DLC/DLC_CON_END/PCConsoleTOC.bin /BIOGame/DLC/DLC_CON_END/CookedPCConsole/Mount.dlc /BIOGame/DLC/DLC_CON_END/CookedPCConsole/Default_DLC_CON_END.bin /BIOGame/DLC/DLC_CON_END/CookedPCConsole/ConditionalsDLC_CON_END.cnd /BIOGame/DLC/DLC_CON_END/CookedPCConsole/ConditionalsDLC_Shared.cnd /BIOGame/DLC/DLC_CON_END/CookedPCConsole/DLC_CON_END_DEU.tlk /BIOGame/DLC/DLC_CON_END/CookedPCConsole/DLC_CON_END_ESN.tlk /BIOGame/DLC/DLC_CON_END/CookedPCConsole/DLC_CON_END_FRA.tlk
To find the name of an entry in the File Table, each of the above lines have to be computed as an MD5 hash. Each character in a line must be sanitized using the following case statement:
public static char Sanitize(char c) { switch ((ushort)c) { case 0x008C: return (char)0x9C; case 0x009F: return (char)0xFF; case 0x00D0: case 0x00DF: case 0x00F0: case 0x00F7: return c; } if ((c >= 'A' && c <= 'Z') || (c >= 'À' && c <= 'Þ')) return char.ToLowerInvariant(c); return c; }
Finally, each sanitized character is casted as byte, added to a byte array, then the whole array encrypted as an MD5 hash.
public static byte[] ComputeHash(string input) { byte[] bytes = new byte[input.Length]; for (int i = 0; i < input.Length; i++) bytes[i] = (byte)Sanitize(input[i]); var md5 = System.Security.Cryptography.MD5.Create(); return md5.ComputeHash(bytes); }
The resulting hash can then be matched to those of each entry in the File Table to assign the corresponding file name.