Source code for ndspy.narc

# Copyright 2019 RoadrunnerWMC
#
# This file is part of ndspy.
#
# ndspy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ndspy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ndspy.  If not, see <https://www.gnu.org/licenses/>.
"""
Support for NARC archives.
"""


import collections
import struct

from . import _common
from . import fnt


[docs]class NARC: """ A class representing a NARC archive file. """ def __init__(self, data=None): self.filenames = fnt.Folder() self.files = [] self.endiannessOfBeginning = '<' if data is not None: self._initFromData(data) def _initFromData(self, data): """ Read NARC data, and create a filename table and a list of files. """ # Read the standard header magic, bom, version, filesize, headersize, numblocks = \ _common.NDS_STD_FILE_HEADER.unpack_from(data, 0) # Little-endian beginnings are in e.g. Spirit Tracks # Big-endian beginnings are in e.g. NSMB if bom == 0xFFFE: self.endiannessOfBeginning = '>' version = (version & 0xFF) << 8 | version >> 8 if version != 1: raise ValueError(f'Unsupported NARC version: {version}') if magic != b'NARC': raise ValueError("Wrong magic (should be b'NARC', instead found " f'{magic})') # Read the file allocation block fatbMagic, fatbSize, fileCount = struct.unpack_from('<4sII', data, 0x10) assert fatbMagic == b'FATB'[::-1], f'Incorrect NARC FATB magic ({fatbMagic})' # Read the file name block fntbOffset = 0x10 + fatbSize fntbMagic, fntbSize = struct.unpack_from('<4sI', data, fntbOffset) assert fntbMagic == b'FNTB'[::-1], f'Incorrect NARC FNTB magic ({fntbMagic})' # Get the data from the file data block before continuing fimgOffset = fntbOffset + fntbSize fimgMagic, gmifSize = struct.unpack_from('<4sI', data, fimgOffset) assert fimgMagic == b'FIMG'[::-1], f'Incorrect NARC FIMG magic ({fimgMagic})' rawDataOffset = fimgOffset + 8 # Read the file datas self.files = [] for i in range(fileCount): startOffset, endOffset = struct.unpack_from('<II', data, 0x1C + 8 * i) self.files.append(data[rawDataOffset+startOffset : rawDataOffset+endOffset]) # Parse the filenames self.filenames = fnt.load(data[fntbOffset + 8 : fntbOffset + fntbSize])
[docs] @classmethod def fromFilesAndNames(cls, files, filenames=None): """ Create a NARC archive from a list of files and (optionally) a filename table. """ self = cls() self.files = files if filenames is not None: self.filenames = filenames return self
[docs] @classmethod def fromFile(cls, filePath, *args, **kwargs): """ Load a NARC archive from a filesystem file. """ with open(filePath, 'rb') as f: return cls(f.read(), *args, **kwargs)
[docs] def save(self): """ Generate file data representing this NARC. """ # Prepare the filedata and file allocation table block fimgData = bytearray(8) fatbData = bytearray() fatbData.extend(struct.pack('<4sII', b'FATB'[::-1], 0x0C + 8 * len(self.files), len(self.files))) # Write data into the FIMG and FAT blocks for i, fd in enumerate(self.files): startOff = len(fimgData) - 8 fimgData.extend(fd) endOff = startOff + len(fd) fatbData.extend(struct.pack('<II', startOff, endOff)) while len(fimgData) % 4: fimgData.append(0) # Put the header on the FIMG block struct.pack_into('<4sI', fimgData, 0, b'FIMG'[::-1], len(fimgData)) # Assemble the filename table block nameTable = bytearray(fnt.save(self.filenames)) while len(nameTable) % 4: nameTable.append(0xFF) fntbData = struct.pack('<4sI', b'FNTB'[::-1], len(nameTable) + 8) + nameTable # Put everything together and return. data = bytearray(0x10) data.extend(fatbData) data.extend(fntbData) data.extend(fimgData) bom = 0xFEFF version = 1 if self.endiannessOfBeginning == '>': bom = 0xFFFE version = 0x100 _common.NDS_STD_FILE_HEADER.pack_into( data, 0, b'NARC', bom, version, len(data), 0x10, 3) return bytes(data)
[docs] def saveToFile(self, filePath): """ Generate file data representing this NARC, and save it to a filesystem file. """ d = self.save() with open(filePath, 'wb') as f: f.write(d)
[docs] def getFileByName(self, filename): """ Return the data for the file with the given filename (path). This is a convenience function. """ fid = self.filenames.idOf(filename) if fid is None: raise ValueError(f'Cannot find file ID of "{filename}"') return self.files[fid]
[docs] def setFileByName(self, filename, data): """ Replace the data for the file with the given filename (path) with the given data. This is a convenience function. """ fid = self.filenames.idOf(filename) if fid is None: raise ValueError(f'Cannot find file ID of "{filename}"') self.files[fid] = data
def __str__(self): notes = [] if self.endiannessOfBeginning != '<': notes.append(f'endiannessOfBeginning={self.endiannessOfBeginning!r}') if notes: notes.insert(0, '') # so it'll begin with a space notes = ' '.join(notes) filenames = '\n '.join(self.filenames._strList(0, self.files)) return f'<narc{notes}\n {filenames}\n>' def __repr__(self): if self.files: try: file0repr = _common.shortBytesRepr(self.files[0]) except Exception: file0repr = "b'...'" more = ', ...' if len(self.files) > 1 else '' files = f'[{file0repr}{more}]' else: files = '[]' return f'{type(self).__name__}.fromFilesAndNames({files}, {self.filenames!r})'