Files
tp/tools/libarc/arc.py
T

541 lines
15 KiB
Python
Raw Normal View History

2021-03-28 22:49:05 +02:00
"""
Simple library for reading and paring rarc files.
"""
import struct
2023-01-23 20:45:57 -07:00
import os
import ctypes
2021-03-28 22:49:05 +02:00
from pathlib import Path, PurePosixPath
2021-03-28 22:49:05 +02:00
from dataclasses import dataclass, field
from typing import List, Dict
#
# source:
# http://wiki.tockdom.com/wiki/RARC_(File_Format)
#
NODE_SIZE = 0x10
DIRECTORY_SIZE = 0x14
2023-01-23 21:33:56 -07:00
ROOT = struct.unpack(">I", "ROOT".encode("ascii"))[0]
2021-03-28 22:49:05 +02:00
def chunks(lst, n):
for i in range(0, len(lst), n):
2023-01-23 21:33:56 -07:00
yield lst[i : i + n]
2021-03-28 22:49:05 +02:00
@dataclass
class StringTable:
2023-01-23 21:33:56 -07:00
"""RARC String Table"""
2021-03-28 22:49:05 +02:00
strings: Dict[int, str] = field(default_factory=dict)
def get(self, offset):
return self.strings[offset]
@dataclass
class Directory:
2023-01-23 21:33:56 -07:00
"""RARC Directory"""
2021-03-28 22:49:05 +02:00
index: int
name_hash: int
type: int
name_offset: int
data_offset: int
data_length: int
unknown0: int
name: str = None
rarc: "RARC" = field(default=None, repr=False)
@dataclass
class File(Directory):
2023-01-23 21:33:56 -07:00
"""RARC File"""
2021-03-28 22:49:05 +02:00
@dataclass
class Folder(Directory):
2023-01-23 21:33:56 -07:00
"""RARC Folder"""
2021-03-28 22:49:05 +02:00
@dataclass
class Node:
2023-01-23 21:33:56 -07:00
"""RARC Node"""
2021-03-28 22:49:05 +02:00
identifier: int
name_offset: int
name_hash: int
directory_count: int
directory_index: int
name: str = None
2023-01-23 20:45:57 -07:00
parent = None
2021-03-28 22:49:05 +02:00
rarc: "RARC" = field(default=None, repr=False)
def files_and_folders(self, depth):
2023-01-23 21:33:56 -07:00
"""Generator for eacg file and directory of this node"""
for directory in self.rarc._directories[self.directory_index :][
: self.directory_count
]:
2021-03-28 22:49:05 +02:00
yield depth, directory
if isinstance(directory, Folder):
if directory.data_offset < len(self.rarc._nodes):
node = self.rarc._nodes[directory.data_offset]
if directory.name == "." or directory.name == "..":
continue
yield from node.files_and_folders(depth + 1)
@dataclass
class RARC:
2023-01-23 21:33:56 -07:00
"""
2021-03-28 22:49:05 +02:00
RARC - Archive of files and folder
"""
# header
magic: int # 'RARC'
file_length: int
header_length: int
file_offset: int
file_data_length: int
2023-01-23 20:45:57 -07:00
file_data_mmem: int
file_data_amem: int
2021-03-28 22:49:05 +02:00
unknown1: int
# info block
node_count: int
node_offset: int
directory_count: int
directory_offset: int
string_table_length: int
string_table_offset: int
file_count: int
unknown2: int
unknown3: int
string_table: StringTable = None
_nodes: List[Node] = field(default_factory=list)
_directories: List[Node] = field(default_factory=list)
_root: Node = None
@property
def files_and_folders(self):
2023-01-23 21:33:56 -07:00
"""Generator for each file and directory"""
2021-03-28 22:49:05 +02:00
yield from self._root.files_and_folders(0)
def read_string_table(rarc, data):
2023-01-23 21:33:56 -07:00
buffer = data[rarc.string_table_offset :][: rarc.string_table_length]
2021-03-28 22:49:05 +02:00
rarc.string_table = StringTable()
offset = 0
2023-01-23 21:33:56 -07:00
for string in str(buffer, "shift-jis").split("\0"):
2021-03-28 22:49:05 +02:00
rarc.string_table.strings[offset] = string
2023-01-23 21:33:56 -07:00
offset += len(bytearray(string, "shift-jis")) + 1
2021-03-28 22:49:05 +02:00
def read_node(rarc, buffer):
2023-01-23 21:33:56 -07:00
node = Node(*struct.unpack(">IIHHI", buffer))
2021-03-28 22:49:05 +02:00
node.name = rarc.string_table.get(node.name_offset)
node.rarc = rarc
return node
def read_nodes(rarc, data):
2023-01-23 21:33:56 -07:00
buffer = data[rarc.node_offset :][: rarc.node_count * NODE_SIZE]
2021-03-28 22:49:05 +02:00
rarc._nodes = []
for node_buffer in chunks(buffer, NODE_SIZE):
node = read_node(rarc, node_buffer)
if node.identifier == ROOT:
rarc._root = node
rarc._nodes.append(node)
def read_directory(rarc, buffer, file_data):
2023-01-23 21:33:56 -07:00
header = struct.unpack(">HHHHIII", buffer)
2021-03-28 22:49:05 +02:00
if header[0] == 0xFFFF:
directory = Folder(*header)
else:
directory = File(*header)
2023-01-23 21:33:56 -07:00
directory.data = file_data[directory.data_offset :][: directory.data_length]
2021-03-28 22:49:05 +02:00
directory.name = rarc.string_table.get(directory.name_offset)
directory.rarc = rarc
return directory
def read_directories(rarc, data, file_data):
2023-01-23 21:33:56 -07:00
buffer = data[rarc.directory_offset :][: rarc.directory_count * DIRECTORY_SIZE]
2021-03-28 22:49:05 +02:00
rarc._directories = []
for directory_buffer in chunks(buffer, DIRECTORY_SIZE):
2023-01-23 21:33:56 -07:00
rarc._directories.append(read_directory(rarc, directory_buffer, file_data))
2021-03-28 22:49:05 +02:00
def read(buffer) -> RARC:
2023-01-23 21:33:56 -07:00
"""Read and parse RARC from buffer."""
2021-03-28 22:49:05 +02:00
# TODO: Add error checking
2023-01-23 21:33:56 -07:00
header = struct.unpack(">IIIIIIII", buffer[:32])
info = struct.unpack(">IIIIIIHHI", buffer[32:][:32])
2021-03-28 22:49:05 +02:00
rarc = RARC(*header, *info)
data = buffer[32:]
2023-01-23 21:33:56 -07:00
file_data = data[rarc.file_offset :][: rarc.file_length]
2021-03-28 22:49:05 +02:00
read_string_table(rarc, data)
read_nodes(rarc, data)
read_directories(rarc, data, file_data)
return rarc
2023-01-23 20:45:57 -07:00
2023-01-23 21:33:56 -07:00
def extract_node(node, arcData, write_function, parentDir, dirNames) -> str:
nodeDir = Path(parentDir) / node.name
if not os.path.exists(nodeDir):
os.mkdir(nodeDir)
2023-01-23 21:33:56 -07:00
for i in range(node.directory_index, node.directory_count + node.directory_index):
2023-01-23 20:45:57 -07:00
dir = arcData._directories[i]
dirNames[i] = str(PurePosixPath(parentDir) / PurePosixPath(node.name)) + "/" + dir.name
2023-01-23 21:33:56 -07:00
if type(dir) == Folder and dir.name != "." and dir.name != "..":
for j, node2 in enumerate(arcData._nodes):
2023-01-23 20:45:57 -07:00
if dir.data_offset == j:
2023-01-23 21:33:56 -07:00
dirNames = extract_node(
node2,
arcData,
write_function,
Path(parentDir) / node.name,
dirNames,
)
2023-01-23 20:45:57 -07:00
break
elif type(dir) == File:
2023-01-23 21:33:56 -07:00
dirNames[i] = write_function(
PurePosixPath(parentDir) / PurePosixPath(node.name) / dir.name, dir.data
2023-01-23 21:33:56 -07:00
)
2023-01-23 20:45:57 -07:00
return dirNames
2023-01-23 21:33:56 -07:00
def extract_to_directory(directory, data, write_function):
print("Extracting " + str(directory))
if not os.path.exists(directory):
os.mkdir(directory)
2023-01-23 20:45:57 -07:00
arcData = read(data)
cwd = os.getcwd()
os.chdir(directory)
2023-01-23 21:33:56 -07:00
dirNames = extract_node(
arcData._root, arcData, write_function, "./", [None] * len(arcData._directories)
)
2023-01-23 20:45:57 -07:00
files_data = ""
2023-01-23 21:33:56 -07:00
for i, dir in enumerate(arcData._directories):
2023-01-23 20:45:57 -07:00
directoryIndicator = ""
specialType = ""
indexToUse = str(dir.index).zfill(len(str(len(arcData._directories))))
if type(dir) == Folder:
directoryIndicator = "/"
indexToUse = "Folder"
if dir.type != 0x200 and dir.type != 0x1100 and dir.type != 0x9500:
2023-01-23 21:33:56 -07:00
specialType = ":" + hex(dir.type)
files_data = (
files_data
+ indexToUse
+ ":"
+ str(dirNames[i])
+ directoryIndicator
+ specialType
+ "\n"
)
2023-01-23 20:45:57 -07:00
fileDataLines = files_data.splitlines()
2023-01-23 21:33:56 -07:00
# fileDataLines.sort(key=lambda x : int(x.split(":")[0]))
filesFile = open("_files.txt", "w", encoding="utf-8")
2023-01-23 20:45:57 -07:00
for line in fileDataLines:
2023-01-23 21:33:56 -07:00
filesFile.write(line + "\n")
2023-01-23 20:45:57 -07:00
os.chdir(cwd)
return directory
2023-01-23 21:33:56 -07:00
2023-01-23 20:45:57 -07:00
def computeHash(string):
hash = 0
for char in string:
2023-01-23 21:33:56 -07:00
hash = hash * 3
2023-01-23 20:45:57 -07:00
hash = hash + ord(char)
hash = ctypes.c_ushort(hash)
hash = hash.value
return hash
2023-01-23 21:33:56 -07:00
2023-01-23 20:45:57 -07:00
def getNodeIdent(fullName):
2023-01-23 21:33:56 -07:00
if len(fullName) < 4:
2023-01-23 20:45:57 -07:00
fullName = fullName.upper()
2023-01-23 21:33:56 -07:00
for i in range(4 - len(fullName)):
2023-01-23 20:45:57 -07:00
fullName = fullName + " "
else:
fullName = fullName.upper()[:4]
2023-01-23 21:33:56 -07:00
return struct.unpack(">I", fullName.encode("ascii"))[0]
2023-01-23 20:45:57 -07:00
2023-01-23 21:33:56 -07:00
def parseDirForPack(
fileDataLines, path, convertFunction, nodes, dirs, currentNode, stringTable, data
):
for i in range(
currentNode.directory_index,
currentNode.directory_count + currentNode.directory_index,
):
2023-01-23 20:45:57 -07:00
currentLine = fileDataLines[i].split(":")
dirId = currentLine[0]
if dirId == "Folder":
dirId = 0xFFFF
else:
dirId = int(dirId)
currentLineName = currentLine[1]
specialDirType = 0
2023-01-23 21:33:56 -07:00
if len(currentLine) > 2:
specialDirType = int(currentLine[2], 16)
if currentLineName[-1] == "/":
2023-01-23 20:45:57 -07:00
currentLineName = currentLineName[0:-1]
dirName = currentLineName.split("/")[-1]
2023-01-23 21:33:56 -07:00
if (
dirName == "."
or dirName == ".."
or (
os.path.isdir(path / currentLineName)
and len(os.path.splitext(dirName)[1]) == 0
)
):
2023-01-23 20:45:57 -07:00
stringTableOffset = 0
nodeIndex = nodes.index(currentNode)
2023-01-23 21:33:56 -07:00
if dirName == "..":
2023-01-23 20:45:57 -07:00
if currentNode.parent == None:
nodeIndex = 0xFFFFFFFF
else:
nodeIndex = nodes.index(currentNode.parent)
stringTableOffset = 2
2023-01-23 21:33:56 -07:00
if dirName != "." and dirName != "..":
stringTableOffset = len(bytearray(stringTable, "shift-jis"))
2023-01-23 20:45:57 -07:00
stringTable = stringTable + dirName + "\0"
dirsInCurrentDir = []
2023-01-23 21:33:56 -07:00
for j, line in enumerate(fileDataLines):
2023-01-23 20:45:57 -07:00
split = line.split(":")[1].split("/")
2023-01-23 21:33:56 -07:00
if split[-1] == "":
2023-01-23 20:45:57 -07:00
split.pop()
if currentLineName == "/".join(split[0:-1]):
dirsInCurrentDir.append(j)
2023-01-23 21:33:56 -07:00
newNode = Node(
getNodeIdent(dirName),
stringTableOffset,
computeHash(dirName),
len(dirsInCurrentDir),
dirsInCurrentDir[0],
dirName,
)
2023-01-23 20:45:57 -07:00
newNode.parent = currentNode
nodes.append(newNode)
2023-01-23 21:33:56 -07:00
nodeIndex = len(nodes) - 1
stringTable, nodes, dirs, data = parseDirForPack(
fileDataLines,
path,
convertFunction,
nodes,
dirs,
newNode,
stringTable,
data,
)
dirs[i] = Folder(
dirId,
computeHash(dirName),
0x200,
stringTableOffset,
nodeIndex,
16,
0,
dirName,
)
2023-01-23 20:45:57 -07:00
else:
2023-01-23 21:33:56 -07:00
realFileName, fileData = convertFunction(currentLineName, path, None, True)
2023-01-23 20:45:57 -07:00
realFileName = os.path.basename(realFileName)
2023-01-23 21:33:56 -07:00
stringTableOffset = len(bytearray(stringTable, "shift-jis"))
2023-01-23 20:45:57 -07:00
stringTable = stringTable + realFileName + "\0"
fileType = 0x1100
2023-01-23 21:33:56 -07:00
if fileData[:4] == bytearray("Yaz0", "utf-8"):
2023-01-23 20:45:57 -07:00
fileType = 0x9500
if specialDirType != 0:
fileType = specialDirType
2023-01-23 21:33:56 -07:00
dirs[i] = File(
dirId,
computeHash(realFileName),
fileType,
stringTableOffset,
len(data),
len(fileData),
0,
realFileName,
)
2023-01-23 20:45:57 -07:00
data = data + fileData
2023-01-23 21:33:56 -07:00
fileEndPadding = 0x20 - (len(data) % 0x20)
2023-01-23 20:45:57 -07:00
if fileEndPadding == 0x20:
fileEndPadding = 0
data = data + bytearray(fileEndPadding)
2023-01-23 21:33:56 -07:00
return stringTable, nodes, dirs, data
2023-01-23 20:45:57 -07:00
2023-01-23 21:33:56 -07:00
def convert_dir_to_arc(sourceDir, convertFunction):
# print("Converting "+str(sourceDir))
fileData = open(sourceDir / "_files.txt", "r", encoding="utf-8").read()
2023-01-23 20:45:57 -07:00
fileDataLinesFull = fileData.splitlines()
2023-01-23 21:33:56 -07:00
# fileDataLinesFull.sort(key=lambda x : int(x.split(":")[0]))
2023-01-23 20:45:57 -07:00
fileDataLines = []
for line in fileDataLinesFull:
2023-01-23 21:33:56 -07:00
# fileDataLines.append(":".join(line.split(":")[1:])) #this should map directory ids to their index directly
2023-01-23 20:45:57 -07:00
fileDataLines.append(line)
rootName = fileDataLines[0].split(":")[1].split("/")[0]
nodes = []
dirs = [None] * len(fileDataLines)
stringTable = ".\0..\0"
2023-01-23 21:33:56 -07:00
nodes.append(
Node(
getNodeIdent("ROOT"),
len(stringTable),
computeHash(rootName),
len(os.listdir(sourceDir / rootName)) + 2,
0,
rootName,
)
)
stringTable = stringTable + rootName + "\0"
2023-01-23 20:45:57 -07:00
data = bytearray(0)
2023-01-23 21:33:56 -07:00
stringTable, nodes, dirs, data = parseDirForPack(
fileDataLines,
sourceDir,
convertFunction,
nodes,
dirs,
nodes[0],
stringTable,
data,
)
dirOffset = 32 + (len(nodes) * 16)
dirOffsetPadding = 0x20 - (dirOffset % 0x20)
2023-01-23 20:45:57 -07:00
if dirOffsetPadding == 0x20:
dirOffsetPadding = 0
dirOffset = dirOffset + dirOffsetPadding
2023-01-23 21:33:56 -07:00
stringTableOffset = dirOffset + (len(dirs) * 20)
stringTablePadding = 0x20 - (stringTableOffset % 0x20)
2023-01-23 20:45:57 -07:00
stringTableOffset = stringTableOffset + stringTablePadding
2023-01-23 21:33:56 -07:00
stringTableLen = len(bytearray(stringTable, "shift-jis"))
fileOffset = stringTableOffset + stringTableLen
fileOffsetPadding = 0x20 - (fileOffset % 0x20)
2023-01-23 20:45:57 -07:00
if fileOffsetPadding == 0x20:
fileOffsetPadding = 0
fileOffset = fileOffset + fileOffsetPadding
2023-01-23 21:33:56 -07:00
fileLength = fileOffset + len(data)
2023-01-23 20:45:57 -07:00
mMemLength = len(data)
aMemLength = 0
2023-01-23 21:33:56 -07:00
2023-01-23 20:45:57 -07:00
fileCount = len(dirs)
folderCount = 0
for dir in dirs:
if type(dir) == Folder:
folderCount = folderCount + 1
if aMemLength == 0 and dir.type == 0xA500:
aMemLength = mMemLength
mMemLength = 0
2023-01-23 21:33:56 -07:00
# hacky way to detect rels.arc
2023-01-23 20:45:57 -07:00
if folderCount == 2:
2023-01-23 21:33:56 -07:00
fileCount = fileCount - 2 # need to check on the logic for this
2023-01-23 20:45:57 -07:00
2023-01-23 21:33:56 -07:00
arcHeader = RARC(
1380012611,
fileLength,
32,
fileOffset,
len(data),
mMemLength,
aMemLength,
0,
len(nodes),
32,
len(dirs),
dirOffset,
stringTableLen + stringTablePadding,
stringTableOffset,
fileCount,
256,
0,
)
headerData = struct.pack(
">IIIIIIIIIIIIIIHHI",
1380012611,
fileLength,
32,
fileOffset,
len(data),
mMemLength,
aMemLength,
0,
len(nodes),
32,
len(dirs),
dirOffset,
stringTableLen + fileOffsetPadding,
stringTableOffset,
fileCount,
256,
0,
)
2023-01-23 20:45:57 -07:00
nodeData = bytearray()
for node in nodes:
2023-01-23 21:33:56 -07:00
nodeData = nodeData + struct.pack(
">IIHHI",
node.identifier,
node.name_offset,
node.name_hash,
node.directory_count,
node.directory_index,
)
2023-01-23 20:45:57 -07:00
dirOffsetPaddingData = bytearray(dirOffsetPadding)
dirData = bytearray()
for dir in dirs:
2023-01-23 21:33:56 -07:00
dirData = dirData + struct.pack(
">HHHHIII",
dir.index,
dir.name_hash,
dir.type,
dir.name_offset,
dir.data_offset,
dir.data_length,
dir.unknown0,
)
2023-01-23 20:45:57 -07:00
stringTablePaddingData = bytearray(stringTablePadding)
2023-01-23 21:33:56 -07:00
stringTableData = bytearray(stringTable, "shift-jis")
2023-01-23 20:45:57 -07:00
fileOffsetPaddingData = bytearray(fileOffsetPadding)
fullData = bytearray()
2023-01-23 21:33:56 -07:00
fullData = (
headerData
+ nodeData
+ dirOffsetPaddingData
+ dirData
+ stringTablePaddingData
+ stringTableData
+ fileOffsetPaddingData
+ data
)
2023-01-23 20:45:57 -07:00
return fullData