public static void create(string[] _inNames, CopyOnlyStream[] _inStreams, string outputPath) { bool _usingFilenames = (_inNames != null); NarcArchiveRootDirectoryEntry _root = new NarcArchiveRootDirectoryEntry(); if (_usingFilenames) { // make all the parent directory thingies and then shove the file entries into them. for (int i = 0; i < _inStreams.Length; ++i) { NarcArchiveDirectoryEntry _curParent = _root; int _startSearchIndex = 0; while (true) { int _nextSlashIndex = _inNames[i].IndexOf('/', _startSearchIndex, _inNames[i].Length - _startSearchIndex); if (_nextSlashIndex != -1) { String _curFolderName = _inNames[i].Substring(_startSearchIndex, _nextSlashIndex - _startSearchIndex); _curParent = insertFolderInto(_curParent, _curFolderName); _startSearchIndex = _nextSlashIndex + 1; } else { break; } } _curParent.Entries.Add(new NarcArchiveFileEntry { dataStream = _inStreams[i], Name = Path.GetFileName(_inNames[i]) }); } } else { for (int i = 0; i < _inStreams.Length; ++i) { _root.Entries.Add(new NarcArchiveFileEntry { dataStream = _inStreams[i] }); } } lowCreate(_root, outputPath, _usingFilenames); }
public static void lowCreate(NarcArchiveRootDirectoryEntry rootDirectory, string outputPath, bool hasFilenames = true) { var directories = new List <NarcArchiveDirectoryEntry> { rootDirectory, }; var files = new List <NarcArchiveFileEntry>(); var directoryIndex = 1; // The root directory index is always 0. var fileIndex = 0; var position = 0; for (var i = 0; i < directories.Count; i++) // We'll be modifying directories during the loop, so we can't use a foreach loop here. { foreach (var entry in directories[i].Entries) { if (entry is NarcArchiveDirectoryEntry directoryEntry) { directoryEntry.Index = directoryIndex; directoryEntry.FirstFileIndex = fileIndex; directories.Add(directoryEntry); directoryIndex++; } else if (entry is NarcArchiveFileEntry fileEntry) { var length = (int)fileEntry.dataStream.getLength(); fileEntry.Index = fileIndex; fileEntry.Offset = position; fileEntry.Length = length; position += ((length + 3) / 4) * 4; // Offsets must be a multiple of 4 files.Add(fileEntry); fileIndex++; } } } var fimgLength = position; using (var output = new FileStream(outputPath, FileMode.Create, FileAccess.Write)) using (var writer = new BinaryWriter(output)) { // Write out the NARC header writer.Write((byte)'N'); writer.Write((byte)'A'); writer.Write((byte)'R'); writer.Write((byte)'C'); writer.Write((byte)0xFE); writer.Write((byte)0xFF); writer.Write((byte)0); writer.Write((byte)1); writer.Write(0); // File length (will be written to later) writer.Write((short)16); // Header length (always 16) writer.Write((short)3); // Number of sections (always 3) // Write out the FATB section writer.Write((byte)'B'); writer.Write((byte)'T'); writer.Write((byte)'A'); writer.Write((byte)'F'); writer.Write(12 + (files.Count * 8)); // Section length writer.Write(files.Count); // Number of file entries foreach (var file in files) { writer.Write(file.Offset); // Start position writer.Write(file.Offset + file.Length); // End position } // Write out the FNTB section writer.Write((byte)'B'); writer.Write((byte)'T'); writer.Write((byte)'N'); writer.Write((byte)'F'); if (hasFilenames) { var fntbPosition = (int)output.Position - 4; writer.Write(0); // Section length (will be written to later) writer.Write(0); // Name entry offset for the root directory (will be written to later) writer.Write((short)0); // First file index (always 0) writer.Write((short)directories.Count); // Number of directories, including the root directory for (var i = 1; i < directories.Count; i++) { writer.Write(0); // Name entry offset for this directory (will be written to later) writer.Write((short)directories[i].FirstFileIndex); // Index of the first file in this directory writer.Write((short)(directories[i].Parent.Index | 0xF000)); // Parent directory } position = directories.Count * 8; foreach (var directory in directories) { directory.NameEntryOffset = position; foreach (var entry in directory.Entries) { var nameAsBytes = Encoding.UTF8.GetBytes(entry.Name); if (entry is NarcArchiveDirectoryEntry directoryEntry) { writer.Write((byte)(nameAsBytes.Length | 0x80)); // Length of the directory name writer.Write(nameAsBytes); writer.Write((short)(directoryEntry.Index | 0xF000)); position += nameAsBytes.Length + 3; } else if (entry is NarcArchiveFileEntry fileEntry) { writer.Write((byte)nameAsBytes.Length); // Length of the file name writer.Write(nameAsBytes); position += nameAsBytes.Length + 1; } } writer.Write((byte)0); position++; } while (output.Length % 4 != 0) { writer.Write((byte)0xFF); } var fntbLength = (int)output.Position - fntbPosition; // Go back and write the name entry offsets for each directory output.Position = fntbPosition + 4; writer.Write(fntbLength); foreach (var directory in directories) { writer.Write(directory.NameEntryOffset); output.Position += 4; } output.Position = fntbPosition + fntbLength; } else { // The FNTB section is always the same if there are no filenames writer.Write(16); // Section length (always 16) writer.Write(4); // Always 4 writer.Write((short)0); // First file index (always 0) writer.Write((short)1); // Number of directories, including the root directory (always 1) } // Write out the FIMG section writer.Write((byte)'G'); writer.Write((byte)'M'); writer.Write((byte)'I'); writer.Write((byte)'F'); writer.Write(fimgLength + 8); // Section length foreach (var file in files) { file.dataStream.CopyTo(output); while (output.Length % 4 != 0) { writer.Write((byte)0xFF); } } // Go back and write out the file length output.Position = 8; writer.Write((int)output.Length); // File length output.Position = output.Length; } }
// returns tuple with: // fimgPosition // hasFilenames // fileEntries public static Tuple <long, bool, List <NarcArchiveFileEntry> > startReadingNarc(FileStream input, BinaryReader reader, string inputPathForErrorMessages, bool ignoreFilenames) { // Read the NARC header if (!(reader.ReadByte() == 'N' && reader.ReadByte() == 'A' && reader.ReadByte() == 'R' && reader.ReadByte() == 'C' && reader.ReadByte() == 0xFE && reader.ReadByte() == 0xFF && reader.ReadByte() == 0 && reader.ReadByte() == 1 && reader.ReadInt32() == input.Length)) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPathForErrorMessages))); } var headerLength = reader.ReadInt16(); var fatbPosition = headerLength; // Read the FATB section input.Position = fatbPosition; if (!(reader.ReadByte() == 'B' && reader.ReadByte() == 'T' && reader.ReadByte() == 'A' && reader.ReadByte() == 'F')) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPathForErrorMessages))); } var fatbLength = reader.ReadInt32(); var fntbPosition = fatbPosition + fatbLength; var fileEntryCount = reader.ReadInt32(); var fileEntries = new List <NarcArchiveFileEntry>(fileEntryCount); for (var i = 0; i < fileEntryCount; i++) { var offset = reader.ReadInt32(); var length = reader.ReadInt32() - offset; fileEntries.Add(new NarcArchiveFileEntry { Offset = offset, Length = length, }); } // Read the FNTB section input.Position = fntbPosition; if (!(reader.ReadByte() == 'B' && reader.ReadByte() == 'T' && reader.ReadByte() == 'N' && reader.ReadByte() == 'F')) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPathForErrorMessages))); } var fntbLength = reader.ReadInt32(); long fimgPosition = fntbPosition + fntbLength; var hasFilenames = !ignoreFilenames; // If the FNTB length is 16 or less, it's impossible for the entries to have filenames. // This section will always be at least 16 bytes long, but technically it's only required to be at least 8 bytes long. if (fntbLength <= 16) { hasFilenames = false; } var rootNameEntryOffset = reader.ReadInt32(); // If the root name entry offset is 4, then the entries don't have filenames. if (rootNameEntryOffset == 4) { hasFilenames = false; } if (hasFilenames) { var rootFirstFileIndex = reader.ReadInt16(); var rootDirectory = new NarcArchiveRootDirectoryEntry(); var directoryEntryCount = reader.ReadInt16(); // This includes the root directory var directoryEntries = new List <NarcArchiveDirectoryEntry>(directoryEntryCount) { rootDirectory, }; // This NARC contains filenames and directory names, so read them for (var i = 1; i < directoryEntryCount; i++) { var nameEntryTableOffset = reader.ReadInt32(); var firstFileIndex = reader.ReadInt16(); var parentDirectoryIndex = reader.ReadInt16() & 0xFFF; directoryEntries.Add(new NarcArchiveDirectoryEntry { Index = i, Parent = directoryEntries[parentDirectoryIndex], NameEntryOffset = nameEntryTableOffset, FirstFileIndex = firstFileIndex, }); } NarcArchiveDirectoryEntry currentDirectory = rootDirectory; var directoryIndex = 0; var fileIndex = 0; while (directoryIndex < directoryEntryCount) { var entryNameLength = reader.ReadByte(); if ((entryNameLength & 0x80) != 0) { // This is a directory name entry var entryName = reader.ReadString(entryNameLength & 0x7F); var entryDirectoryIndex = reader.ReadInt16() & 0xFFF; var directoryEntry = directoryEntries[entryDirectoryIndex]; directoryEntry.Name = entryName; } else if (entryNameLength != 0) { // This is a file name entry var entryName = reader.ReadString(entryNameLength); var fileEntry = fileEntries[fileIndex]; fileEntry.Parent = directoryEntries[directoryIndex]; fileEntry.Name = entryName; fileIndex++; } else { // This is the end of a directory directoryIndex++; if (directoryIndex >= directoryEntryCount) { break; } currentDirectory = directoryEntries[directoryIndex]; } } } return(new Tuple <long, bool, List <NarcArchiveFileEntry> >(fimgPosition, hasFilenames, fileEntries)); }
public static void Extract(string inputPath, string outputPath, bool ignoreFilenames = false, bool _justPrintNames = false) { using (var input = new FileStream(inputPath, FileMode.Open, FileAccess.Read)) using (var reader = new BinaryReader(input)) { // Read the NARC header if (!(reader.ReadByte() == 'N' && reader.ReadByte() == 'A' && reader.ReadByte() == 'R' && reader.ReadByte() == 'C' && reader.ReadByte() == 0xFE && reader.ReadByte() == 0xFF && reader.ReadByte() == 0 && reader.ReadByte() == 1 && reader.ReadInt32() == input.Length)) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPath))); } var headerLength = reader.ReadInt16(); var fatbPosition = headerLength; // Read the FATB section input.Position = fatbPosition; if (!(reader.ReadByte() == 'B' && reader.ReadByte() == 'T' && reader.ReadByte() == 'A' && reader.ReadByte() == 'F')) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPath))); } var fatbLength = reader.ReadInt32(); var fntbPosition = fatbPosition + fatbLength; var fileEntryCount = reader.ReadInt32(); var fileEntries = new List <NarcArchiveFileEntry>(fileEntryCount); for (var i = 0; i < fileEntryCount; i++) { var offset = reader.ReadInt32(); var length = reader.ReadInt32() - offset; fileEntries.Add(new NarcArchiveFileEntry { Offset = offset, Length = length, }); } // Read the FNTB section input.Position = fntbPosition; if (!(reader.ReadByte() == 'B' && reader.ReadByte() == 'T' && reader.ReadByte() == 'N' && reader.ReadByte() == 'F')) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPath))); } var fntbLength = reader.ReadInt32(); var fimgPosition = fntbPosition + fntbLength; var hasFilenames = !ignoreFilenames; // If the FNTB length is 16 or less, it's impossible for the entries to have filenames. // This section will always be at least 16 bytes long, but technically it's only required to be at least 8 bytes long. if (fntbLength <= 16) { hasFilenames = false; } var rootNameEntryOffset = reader.ReadInt32(); // If the root name entry offset is 4, then the entries don't have filenames. if (rootNameEntryOffset == 4) { hasFilenames = false; } if (hasFilenames) { var rootFirstFileIndex = reader.ReadInt16(); var rootDirectory = new NarcArchiveRootDirectoryEntry(); var directoryEntryCount = reader.ReadInt16(); // This includes the root directory var directoryEntries = new List <NarcArchiveDirectoryEntry>(directoryEntryCount) { rootDirectory, }; // This NARC contains filenames and directory names, so read them for (var i = 1; i < directoryEntryCount; i++) { var nameEntryTableOffset = reader.ReadInt32(); var firstFileIndex = reader.ReadInt16(); var parentDirectoryIndex = reader.ReadInt16() & 0xFFF; directoryEntries.Add(new NarcArchiveDirectoryEntry { Index = i, Parent = directoryEntries[parentDirectoryIndex], NameEntryOffset = nameEntryTableOffset, FirstFileIndex = firstFileIndex, }); } NarcArchiveDirectoryEntry currentDirectory = rootDirectory; var directoryIndex = 0; var fileIndex = 0; while (directoryIndex < directoryEntryCount) { var entryNameLength = reader.ReadByte(); if ((entryNameLength & 0x80) != 0) { // This is a directory name entry var entryName = reader.ReadString(entryNameLength & 0x7F); var entryDirectoryIndex = reader.ReadInt16() & 0xFFF; var directoryEntry = directoryEntries[entryDirectoryIndex]; directoryEntry.Name = entryName; if (_justPrintNames) { Console.WriteLine("Dir: " + entryName); } } else if (entryNameLength != 0) { // This is a file name entry var entryName = reader.ReadString(entryNameLength); var fileEntry = fileEntries[fileIndex]; fileEntry.Directory = directoryEntries[directoryIndex]; fileEntry.Name = entryName; fileIndex++; if (_justPrintNames) { Console.WriteLine(entryName); } } else { // This is the end of a directory directoryIndex++; if (directoryIndex >= directoryEntryCount) { break; } currentDirectory = directoryEntries[directoryIndex]; } } } if (_justPrintNames) { return; } // Read the FIMG section input.Position = fimgPosition; if (!(reader.ReadByte() == 'G' && reader.ReadByte() == 'M' && reader.ReadByte() == 'I' && reader.ReadByte() == 'F')) { throw new InvalidFileTypeException(string.Format(ErrorMessages.NotANarcFile, Path.GetFileName(inputPath))); } if (hasFilenames) { foreach (var fileEntry in fileEntries) { var entryOutputPath = Path.Combine(outputPath, fileEntry.FullName); var entryOutputDirectory = Path.GetDirectoryName(entryOutputPath); if (!Directory.Exists(entryOutputDirectory)) { Directory.CreateDirectory(entryOutputDirectory); } using (var output = new FileStream(entryOutputPath, FileMode.Create, FileAccess.Write)) using (var entryStream = new SubReadStream(input, fimgPosition + 8 + fileEntry.Offset, fileEntry.Length)) { entryStream.CopyTo(output); } } } else { // This NARC doesn't contain filenames and directory names, so just use a file index as their filename var index = 0; var numOfDigits = Math.Floor(Math.Log10(fileEntryCount) + 1); foreach (var fileEntry in fileEntries) { var entryName = $"{Path.GetFileNameWithoutExtension(inputPath)}_{index.ToString($"D{numOfDigits}")}"; var entryOutputPath = Path.Combine(outputPath, entryName); if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } using (var output = new FileStream(entryOutputPath, FileMode.Create, FileAccess.Write)) using (var entryStream = new SubReadStream(input, fimgPosition + 8 + fileEntry.Offset, fileEntry.Length)) { entryStream.CopyTo(output); } index++; } } } }