Skip to content

Instantly share code, notes, and snippets.

@Kyuuhachi
Last active April 7, 2024 15:21

Revisions

  1. Kyuuhachi revised this gist Dec 26, 2023. 1 changed file with 4 additions and 9 deletions.
    13 changes: 4 additions & 9 deletions nani.py
    Original file line number Diff line number Diff line change
    @@ -21,15 +21,10 @@ def decode(data: bytes) -> bytes:
    return bytes(o)

    def fnhash(name: bytes) -> int:
    v = 0
    for i, b in enumerate(name):
    v += (b - 32) * (1<<(i%5*5))
    # There's gotta be a better way to do this...
    v = (v & 0xFFFF) + (v >> 16) * 15
    v = (v & 0xFFFF) + (v >> 16) * 15
    v = (v & 0xFFFF) + (v >> 16) * 15
    if v > 0xFFF0: v = (v + 0xF) & 0xFFFF
    return v
    return sum(
    (b - 32) * (1<<(i%5*5))
    for i, b in enumerate(name)
    ) % 0xFFF1

    def extract(na: bytes, ni: bytes, *, verify: bool = False) -> T.Iterator[tuple[str, bytes]]:
    def take(a: bytes, n: int) -> tuple[bytes, bytes]: return a[:n], a[n:]
  2. Kyuuhachi created this gist Dec 26, 2023.
    87 changes: 87 additions & 0 deletions nani.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,87 @@
    import typing as T
    from pathlib import Path
    import zlib
    import struct

    try:
    import numpy as np
    def decode(data: bytes) -> bytes:
    data = np.frombuffer(data, dtype=np.ubyte).copy()
    obf = 0x7C53F961 * 0x3D09 ** (1+np.arange(len(data), dtype=np.uint32))
    data -= obf >> 16
    return data.tobytes()
    except ImportError:
    print("Could not import numpy — falling back to slow method")
    def decode(data: bytes) -> bytes:
    k = 0x7C53F961
    o = bytearray()
    for b in data:
    k *= 0x3D09
    o.append((b - (k >> 16)) & 0xFF)
    return bytes(o)

    def fnhash(name: bytes) -> int:
    v = 0
    for i, b in enumerate(name):
    v += (b - 32) * (1<<(i%5*5))
    # There's gotta be a better way to do this...
    v = (v & 0xFFFF) + (v >> 16) * 15
    v = (v & 0xFFFF) + (v >> 16) * 15
    v = (v & 0xFFFF) + (v >> 16) * 15
    if v > 0xFFF0: v = (v + 0xF) & 0xFFFF
    return v

    def extract(na: bytes, ni: bytes, *, verify: bool = False) -> T.Iterator[tuple[str, bytes]]:
    def take(a: bytes, n: int) -> tuple[bytes, bytes]: return a[:n], a[n:]
    head, ni = take(ni, 16)
    head, p2, p3, p4 = struct.unpack("<4sIII", head)
    assert head == b"NNI\0"
    toc, ni = take(ni, p2*16)
    names, ni = take(ni, p3)
    assert p4 & 0x01 == 0, ".\\core\\nnk_file.cpp: vfs::Startup: No incremental linking support."
    assert not ni

    toc = decode(toc)
    names = decode(names)

    for hash, size, pos, namepos in struct.iter_unpack("<IIII", toc):
    name = names[namepos:names.find(b"\0", namepos)]
    if verify: assert hash == fnhash(name), name
    name = name.decode("cp932").lower().replace("\\", "/")

    if (pos, size) == (0, 0):
    print(f"skipping blank file {name}")
    continue

    data = na[pos:pos+size]
    if name.endswith(".z"):
    name = name[:-2]
    try:
    checksum, usize = struct.unpack("<II", data[:8])
    data = zlib.decompress(data[8:])
    assert len(data) == usize
    if verify: assert checksum == zlib.crc32(data)
    except Exception:
    print(f"failed to decompress {name}")
    import traceback
    traceback.print_exc()
    yield name, data

    import argparse
    argp = argparse.ArgumentParser()
    argp.add_argument("-o", "--outdir", type=Path, required=True, dest="outdir")
    argp.add_argument("-v", "--verbose", action="store_true")
    argp.add_argument("-V", "--verify", action="store_true")
    argp.add_argument("files", nargs="+", type=Path)
    def __main__(outdir: Path, verbose: bool, verify: bool, files: list[Path]):
    for path in files:
    assert path.suffix in [".na", ".ni"], path
    na = path.with_suffix(".na").read_bytes()
    ni = path.with_suffix(".ni").read_bytes()
    for name, data in extract(na, ni, verify=verify):
    out = outdir / name
    if verbose: print(out)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_bytes(data)
    if __name__ == "__main__":
    __main__(**argp.parse_args().__dict__)