├── .gitignore ├── README.md └── darc.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nintendo darc tool 2 | ![Python](https://img.shields.io/badge/Python-2.7-blue) 3 | 4 | The "darc" file format was found in the early version of Nintendo Ware for CTR (nw4c). 5 | 6 | This tool can help you extract existing darc files and create new darc files. 7 | 8 | # Usage samples 9 | Extract "input.arc" to "output" folder: 10 | 11 | ``` shell 12 | python darc.py -xf input.arc -d output 13 | ``` 14 | 15 | Change directory to "input/" and add entries recursively to "output.arc": 16 | ``` shell 17 | python darc.py -cf output.arc -d input dirs files ... 18 | ``` 19 | 20 | Create darc with all file data align to 32: 21 | ``` shell 22 | python darc.py -c -a 0x20 -f output.arc -d input dirs files ... 23 | ``` 24 | 25 | Create darc with *.bcfnt files data align to 128, other files align to 32: 26 | ``` shell 27 | python darc.py -c -a 0x20 -t *.bcfnt:0x80 -f output.arc -d input dirs files ... 28 | ``` 29 | 30 | # Notes 31 | By default, this tool creates archives without the "." entry, which is commonly found in many games. 32 | If you need that entry, you just need to add a "." as the input entry. For example: 33 | ``` shell 34 | python darc.py -c -f output.arc -d input . 35 | ``` 36 | Note that there's a dot at the end of the command -------------------------------------------------------------------------------- /darc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from argparse import ArgumentError 3 | import codecs 4 | from fnmatch import fnmatch 5 | from io import BytesIO 6 | from operator import truediv 7 | import os 8 | from os.path import join 9 | import struct 10 | 11 | 12 | class FormatError(Exception): 13 | def __init__(self, *args): 14 | super(FormatError, self).__init__(*args) 15 | 16 | 17 | class DarcEntry(object): 18 | def __init__(self, byte_order): 19 | super(DarcEntry, self).__init__() 20 | self.byte_order = byte_order 21 | 22 | # Create a root entry by default. 23 | self.__parameters__ = [ 24 | 0x01000000, 25 | 0, 26 | 1 # At least 1 entry (the root entry) in the archive. 27 | ] 28 | self.name = '' 29 | self.parent = None 30 | self.children = [] 31 | self.__data__ = None 32 | self.__darc_file__ = None 33 | 34 | def addchild(self, child): 35 | if not isinstance(child, DarcEntry): 36 | raise TypeError() 37 | 38 | child.parent = self 39 | self.children.append(child) 40 | 41 | def ischildof(self, entry): 42 | e = self 43 | while e.parent != None: 44 | if e.parent is entry: 45 | return True 46 | e = e.parent 47 | return False 48 | 49 | def load(self, f): 50 | self.__darc_file__ = f 51 | s = f.read(0xC) 52 | self.__parameters__ = list( 53 | struct.unpack_from(self.byte_order+'iii', s)) 54 | 55 | def tobin(self): 56 | return struct.pack(self.byte_order+'iii', *self.__parameters__) 57 | 58 | def __repr__(self): 59 | return '"{name}" '.format(name=self.name, isdir=self.isdir, l=self.length) 60 | 61 | @property 62 | def data(self): 63 | if self.__data__: 64 | return self.__data__ 65 | 66 | if self.__darc_file__: 67 | self.__darc_file__.seek(self.data_offset, 0) 68 | self.__data__ = self.__darc_file__.read(self.length) 69 | 70 | return self.__data__ 71 | 72 | @data.setter 73 | def data(self, value): 74 | self.__data__ = value 75 | 76 | @property 77 | def isdir(self): 78 | return (self.__parameters__[0] & 0x01000000) == 0x01000000 79 | 80 | @isdir.setter 81 | def isdir(self, value): 82 | if value: 83 | self.__parameters__[0] |= 0x01000000 84 | else: 85 | self.__parameters__[0] &= 0xFEFFFFFF 86 | 87 | @property 88 | def data_offset(self): 89 | return self.__parameters__[1] 90 | 91 | @data_offset.setter 92 | def data_offset(self, value): 93 | if not isinstance(value, int) and not isinstance(value, long): 94 | raise TypeError() 95 | 96 | self.__parameters__[1] = value 97 | 98 | @property 99 | def name_offset(self): 100 | return self.__parameters__[0] & 0x00FFFFFF 101 | 102 | @name_offset.setter 103 | def name_offset(self, value): 104 | if not isinstance(value, int) and not isinstance(value, long): 105 | raise TypeError("Name offset must be integer.") 106 | 107 | if value > 0x00FFFFFF: 108 | raise ValueError( 109 | "Name offset must not lager than %d" % (0x00FFFFFF)) 110 | 111 | self.__parameters__[0] &= 0xFF000000 112 | self.__parameters__[0] |= (0x00FFFFFF & value) 113 | 114 | @property 115 | def length(self): 116 | return self.__parameters__[2] 117 | 118 | @length.setter 119 | def length(self, value): 120 | if isinstance(value, int): 121 | self.__parameters__[2] = value 122 | else: 123 | raise TypeError() 124 | 125 | @property 126 | def fullpath(self): 127 | e = self 128 | path = e.name 129 | while e.parent != None: 130 | path = e.parent.name + os.path.sep + path 131 | e = e.parent 132 | return path 133 | 134 | 135 | class Darc(object): 136 | DARC_HEADER_MAGIC = 'darc' 137 | DARC_HEADER_STRUCT = 'hiiiii' 138 | DARC_SUPPORTED_VERSION = 0x01000000 139 | 140 | def __init__(self, byte_order='<', alignment=4, typealign=[]): 141 | super(Darc, self).__init__() 142 | (self.header_size, self.version, self.file_size, self.file_table_offset, 143 | self.file_table_size, self.file_data_offset) = ( 144 | 0x1C, self.DARC_SUPPORTED_VERSION, 0, 0x1C, 0, 0 145 | ) 146 | self.byte_order = byte_order 147 | self.root_entry = DarcEntry(self.byte_order) 148 | self.defaultalignment = alignment 149 | self.typealignment = typealign 150 | 151 | def __buildindex__(self): 152 | entry_stack = [self.root_entry] 153 | dir_stack = [] 154 | i = 0 155 | while(len(entry_stack) > 0): 156 | i += 1 157 | entry = entry_stack[-1] 158 | while len(dir_stack) > 0 and not entry.ischildof(dir_stack[-1]): 159 | dir_stack.remove(dir_stack[-1]) 160 | if entry.isdir: 161 | dir_stack.append(entry) 162 | entry_stack.remove(entry_stack[-1]) 163 | for e in entry.children[::-1]: 164 | entry_stack.append(e) 165 | for d in dir_stack: 166 | d.length = i 167 | 168 | def close(self): 169 | if self.darc_file and not self.darc_file.closed: 170 | self.darc_file.close() 171 | 172 | def save(self, path_out): 173 | self.__buildindex__() 174 | entries = self.flatentries 175 | 176 | file_name_table = BytesIO() 177 | file_name_writer = codecs.getwriter('utf-16le')(file_name_table) 178 | for e in entries: 179 | e.name_offset = file_name_table.tell() 180 | file_name_writer.write(e.name) 181 | file_name_writer.write(u'\0') 182 | 183 | file_name_data = file_name_table.getvalue() 184 | self.file_table_size = (len(entries) * 0x0C) + len(file_name_data) 185 | 186 | print ('Save: '+path_out) 187 | with open(path_out, 'wb') as darc: 188 | darc.write(self.DARC_HEADER_MAGIC) 189 | darc.write('\xFF\xFE' if self.byte_order == 190 | '<' else '\xFE\xFF' if self.byte_order == '>' else '\x00\x00') 191 | darc.write(struct.pack(self.byte_order+self.DARC_HEADER_STRUCT, self.header_size, self.DARC_SUPPORTED_VERSION, 192 | self.file_size, self.file_table_offset, self.file_table_size, self.file_data_offset)) 193 | darc.write('\x00'*(0xC*len(entries))) 194 | darc.write(file_name_data) 195 | 196 | first_entry = True 197 | for e in entries: 198 | if e.isdir: 199 | continue 200 | 201 | alignto = align(darc.tell(), self.getalignment(e.name)) 202 | darc.seek(alignto, 0) 203 | 204 | if first_entry: 205 | self.file_data_offset = darc.tell() 206 | first_entry = False 207 | 208 | e.data_offset = darc.tell() 209 | data = e.data 210 | e.length = len(data) 211 | darc.write(data) 212 | 213 | self.file_size = darc.tell() 214 | darc.seek(6, 0) 215 | darc.write(struct.pack(self.byte_order+self.DARC_HEADER_STRUCT, self.header_size, self.DARC_SUPPORTED_VERSION, 216 | self.file_size, self.file_table_offset, self.file_table_size, self.file_data_offset)) 217 | for e in entries: 218 | darc.write(e.tobin()) 219 | 220 | def getalignment(self, name): 221 | for p in self.typealignment: 222 | if fnmatch(name, p[0]): 223 | return p[1] 224 | return self.defaultalignment 225 | 226 | def addentry(self, path, exclude=[]): 227 | if os.path.isfile(path): 228 | self.addfile(path) 229 | elif os.path.isdir(path): 230 | self.adddir(path, exclude) 231 | else: 232 | raise ArgumentError('Unknown path type: '+path) 233 | 234 | def adddir(self, path, exclude=[]): 235 | fsentry_stack = [path] 236 | dir_map = {parentdir(path): self.root_entry} 237 | while(len(fsentry_stack) > 0): 238 | curfsentry = os.path.normpath(fsentry_stack[-1]) 239 | fsentry_stack.remove(fsentry_stack[-1]) 240 | 241 | if should_exclude(curfsentry, exclude): 242 | continue 243 | 244 | entry = DarcEntry(self.byte_order) 245 | entry.name = os.path.split(curfsentry)[-1] 246 | parent = parentdir(curfsentry) 247 | dir_map[parent].addchild(entry) 248 | 249 | if os.path.isdir(curfsentry): 250 | dir_map[os.path.abspath(curfsentry)] = entry 251 | for d in os.listdir(curfsentry)[::-1]: 252 | fsentry_stack.append(os.path.join(curfsentry, d)) 253 | elif os.path.isfile(curfsentry): 254 | entry.isdir = False 255 | with open(curfsentry, 'rb') as f: 256 | print('load: '+curfsentry) 257 | entry.data = f.read() 258 | 259 | def addfile(self, path): 260 | entry = DarcEntry(self.byte_order) 261 | entry.name = os.path.split(path)[1] 262 | entry.isdir = False 263 | with open(path, 'rb') as fs: 264 | entry.data = fs.read() 265 | self.root_entry.addchild(entry) 266 | 267 | @ staticmethod 268 | def fromDir(workdir, byte_order='<', entries=['.'], exclude=[]): 269 | darc = Darc(byte_order) 270 | 271 | origndir = os.getcwd() 272 | os.chdir(workdir) 273 | for n in entries: 274 | darc.addentry(n, exclude) 275 | os.chdir(origndir) 276 | 277 | return darc 278 | 279 | @ staticmethod 280 | def load(path): 281 | darc = Darc() 282 | darc.darc_file = open(path, 'rb') 283 | magic = darc.darc_file.read(4) 284 | if magic != darc.DARC_HEADER_MAGIC: 285 | raise FormatError("Invalid file magic.") 286 | 287 | byte_order_mark = darc.darc_file.read(2) 288 | if byte_order_mark == '\xFF\xFE': 289 | darc.byte_order = '<' 290 | elif byte_order_mark == '\xFE\xFF': 291 | darc.byte_order = '>' 292 | else: 293 | raise FormatError("Invalid byte order mark.") 294 | 295 | (darc.header_size, darc.version, darc.file_size, darc.file_table_offset, 296 | darc.file_table_size, darc.file_data_offset) = struct.unpack(darc.byte_order+darc.DARC_HEADER_STRUCT, darc.darc_file.read(struct.calcsize(darc.byte_order+darc.DARC_HEADER_STRUCT))) 297 | 298 | if darc.version != darc.DARC_SUPPORTED_VERSION: 299 | raise FormatError("Unsupported file version.") 300 | 301 | darc.root_entry = DarcEntry(darc.byte_order) 302 | darc.root_entry.load(darc.darc_file) 303 | 304 | current_offset = darc.darc_file.tell() 305 | file_name_table_offset = darc.total_entries * 0xC + current_offset 306 | file_name_table_size = darc.file_table_size - \ 307 | (darc.total_entries * 0xC) 308 | darc.darc_file.seek(file_name_table_offset, 0) 309 | file_name_table = darc.darc_file.read(file_name_table_size) 310 | darc.darc_file.seek(current_offset, 0) 311 | 312 | dir_list = [darc.root_entry] 313 | for i in xrange(1, darc.total_entries+1): 314 | e = DarcEntry(darc.byte_order) 315 | e.load(darc.darc_file) 316 | e.name = get_unicode_str(file_name_table, e.name_offset) 317 | 318 | dir_list[-1].addchild(e) 319 | 320 | if i >= dir_list[-1].length-1: 321 | dir_list.remove(dir_list[-1]) 322 | 323 | if e.isdir: 324 | dir_list.append(e) 325 | 326 | return darc 327 | 328 | def extract(self, path_out, exclude=[]): 329 | for e in self.flatentries: 330 | fullpath = e.fullpath 331 | if should_exclude(fullpath, exclude): 332 | continue 333 | 334 | if e.isdir: 335 | mkdirs(path_out+fullpath) 336 | else: 337 | fp = path_out+fullpath 338 | if not os.path.exists(os.path.split(fp)[0]): 339 | continue 340 | 341 | data = e.data 342 | with open(fp, 'wb') as of: 343 | print('Extract: '+fp) 344 | of.write(data) 345 | 346 | def list(self, exclude=[]): 347 | for e in self.flatentries: 348 | if should_exclude(e.fullpath, exclude): 349 | continue 350 | print(e.fullpath) 351 | 352 | @ property 353 | def total_entries(self): 354 | return self.root_entry.length - 1 355 | 356 | @ total_entries.setter 357 | def total_entries(self, value): 358 | self.root_entry.length = value + 1 359 | 360 | @ property 361 | def flatentries(self): 362 | entry_stack = [self.root_entry] 363 | entry_list = [] 364 | while(len(entry_stack) > 0): 365 | entry = entry_stack[-1] 366 | entry_list.append(entry) 367 | entry_stack.remove(entry_stack[-1]) 368 | for e in entry.children[::-1]: 369 | entry_stack.append(e) 370 | return entry_list 371 | 372 | 373 | def align(value, alignment): 374 | return (value + alignment - 1) / alignment * alignment 375 | 376 | 377 | def get_unicode_str(data, start_at=0): 378 | ms = BytesIO(data) 379 | reader = codecs.getreader('utf-16le')(ms) 380 | ms.seek(start_at) 381 | 382 | s = [] 383 | while True: 384 | c = reader.read(1) 385 | if c == u'\0': 386 | break 387 | s.append(c) 388 | 389 | return u''.join(s) 390 | 391 | 392 | def mkdirs(path): 393 | if not os.path.exists(path): 394 | print('Create: '+path) 395 | os.makedirs(path) 396 | 397 | 398 | def walk(dirname, pattern='*.*'): 399 | for root, dirs, files in os.walk(dirname): 400 | for filename in files: 401 | if not fnmatch(filename, pattern): 402 | continue 403 | fullname = os.path.join(root, filename) 404 | yield fullname 405 | 406 | 407 | def walkdirs(dirname, pattern='*.*'): 408 | for root, dirs, files in os.walk(dirname): 409 | for dname in dirs: 410 | if not fnmatch(dname, pattern): 411 | continue 412 | fullname = os.path.join(root, dname) 413 | yield fullname 414 | 415 | 416 | def should_exclude(path, exclude): 417 | is_excluded = False 418 | for ex in exclude: 419 | if fnmatch(path, ex): 420 | is_excluded = True 421 | break 422 | return is_excluded 423 | 424 | 425 | def parentdir(path): 426 | return os.path.abspath(os.path.join(path, os.pardir)) 427 | 428 | 429 | def parsetypealignments(types): 430 | config = [] 431 | for t in types: 432 | if t.count(':') != 1: 433 | continue 434 | p = t.split(':') 435 | p = (p[0], int(p[1], 0)) 436 | config.append(p) 437 | return config 438 | 439 | 440 | def main(): 441 | import argparse 442 | 443 | parser = argparse.ArgumentParser(add_help=True) 444 | group = parser.add_mutually_exclusive_group(required=True) 445 | group.add_argument('-c', '--create', help='Create an archive', 446 | action='store_true', default=False) 447 | group.add_argument('-x', '--extract', help='Extract an archive', 448 | action='store_true', default=False) 449 | group.add_argument('-l', '--list', help='List entries of an archive', 450 | action='store_true', default=True) 451 | parser.add_argument( 452 | '-a', '--align', help='Set archive data alignment', type=str, default='4') 453 | parser.add_argument( 454 | '-t', '--typealign', help='Set archive data alignment for specified file type. Argument pattern: *.:', type=str, default=[], nargs='*') 455 | parser.add_argument( 456 | '-d', '--dir', help='Set working directory', default='.') 457 | parser.add_argument('-f', '--file', help='Set archive file') 458 | parser.add_argument('-n', '--exclude', 459 | help='Set exclude files', nargs='*', type=str, default=[]) 460 | parser.add_argument('-e', '--endianess', help='Set archive endianess', 461 | choices=['big', 'little'], type=str, default='little') 462 | parser.add_argument('entry', nargs=argparse.REMAINDER, 463 | default=['.'], type=str) 464 | args = parser.parse_args() 465 | 466 | endianess = {'big': '>', 'little': '<'} 467 | if args.create: 468 | darc = Darc.fromDir( 469 | args.dir, endianess[args.endianess], os.listdir(args.dir) if not args.entry else args.entry, args.exclude) 470 | darc.defaultalignment = int(args.align, 0) 471 | darc.typealignment = parsetypealignments(args.typealign) 472 | darc.save(args.file) 473 | elif args.extract: 474 | darc = Darc.load(args.file) 475 | if args.dir: 476 | darc.extract(args.dir, args.exclude) 477 | elif args.list: 478 | darc = Darc.load(args.file) 479 | darc.list(args.exclude) 480 | 481 | 482 | if __name__ == "__main__": 483 | main() 484 | --------------------------------------------------------------------------------