Task 456: .NRO File Format

Task 456: .NRO File Format

1. List of Properties for the .NRO File Format

The .NRO file format is used for Nintendo Switch homebrew executables. Based on the specifications, the intrinsic properties refer to the structural elements, headers, fields, and optional sections that define the file's layout on disk (e.g., offsets, sizes, signatures, and segment descriptions). These are parsed from the binary data in little-endian byte order. Here's a comprehensive list:

RocrtHeader (at file offset 0x0, size varies by system version):

  • ModuleHeaderLocation: Size 0xC bytes (17.0.0+) or 0x8 bytes (1.0.0-16.1.0); describes the location of the module header.
  • Reserved: 4 bytes.

NroHeader (at file offset 0x10, size 0x70 bytes):

  • Signature: 4 bytes (must be "NRO0").
  • Version: 4 bytes (format version).
  • Size: 4 bytes (size of the main NRO content, excluding optional assets).
  • Flags: 4 bytes (various flags).
  • TextMemoryOffset: 4 bytes (memory offset for the .text segment).
  • TextSize: 4 bytes (size of the .text segment).
  • RoMemoryOffset: 4 bytes (memory offset for the .rodata segment).
  • RoSize: 4 bytes (size of the .rodata segment).
  • DataMemoryOffset: 4 bytes (memory offset for the .data segment).
  • DataSize: 4 bytes (size of the .data segment).
  • BssSize: 4 bytes (size of the .bss segment).
  • Reserved1: 4 bytes (reserved).
  • ModuleId: 32 bytes (build ID from ELF note section).
  • DsoHandleOffset: 4 bytes (DSO handle offset).
  • Reserved2: 4 bytes (reserved).
  • EmbeddedOffset: 4 bytes (offset to embedded data).
  • EmbeddedSize: 4 bytes (size of embedded data).
  • DynStrOffset: 4 bytes (offset to dynamic string table).
  • DynStrSize: 4 bytes (size of dynamic string table).
  • DynSymOffset: 4 bytes (offset to dynamic symbol table).
  • DynSymSize: 4 bytes (size of dynamic symbol table).

Optional Assets Section (starts at file offset equal to NroHeader.Size if present; detected by checking if total file length > NroHeader.Size and magic "ASET" at that offset):

  • AssetHeader (size 0x38 bytes):
  • Magic: 4 bytes (must be "ASET").
  • Version: 4 bytes (asset format version, typically 0).
  • Icon AssetSection: 16 bytes (8-byte offset + 8-byte size for icon data, usually a 256x256 JPEG).
  • NACP AssetSection: 16 bytes (8-byte offset + 8-byte size for NACP metadata).
  • Romfs AssetSection: 16 bytes (8-byte offset + 8-byte size for RomFS data).

These properties define the file's segments, metadata, and optional embedded assets, which are loaded into memory or used by the Nintendo Switch runtime.

3. Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .NRO Parser

This is a self-contained HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML page). It creates a drag-and-drop area where users can drop a .NRO file. The script reads the file as an ArrayBuffer, parses it using DataView (little-endian), extracts all properties from the list above, and dumps them to the screen in a readable format. It handles optional assets detection.

Drag and drop a .NRO file here

4. Python Class for .NRO Handling

This Python class uses struct for decoding. It can open a .NRO file, read and decode the properties, print them to console, and write modifications back (e.g., update version and save a new file).

import struct
import sys
import os

class NROParser:
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = None
        self.properties = {}
        self.has_assets = False
        self.read()

    def read(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()
        if len(self.data) < 0x80:
            raise ValueError("Invalid .NRO file: too small")
        
        offset = 0
        # RocrtHeader
        module_loc = self.data[offset:offset+0xC]
        offset += 0xC
        reserved = struct.unpack_from('<I', self.data, offset)[0]
        offset += 4
        self.properties['RocrtHeader'] = {
            'ModuleHeaderLocation': ' '.join(f'{b:02x}' for b in module_loc),
            'Reserved': hex(reserved)
        }
        
        # NroHeader
        signature = self.data[offset:offset+4].decode('ascii')
        if signature != 'NRO0':
            raise ValueError("Invalid signature")
        offset += 4
        version, size, flags, text_off, text_size, ro_off, ro_size, data_off, data_size, bss_size, reserved1 = struct.unpack_from('<11I', self.data, offset)
        offset += 44
        module_id = self.data[offset:offset+32]
        offset += 32
        dso_handle, reserved2, embed_off, embed_size, dynstr_off, dynstr_size, dynsym_off, dynsym_size = struct.unpack_from('<8I', self.data, offset)
        self.properties['NroHeader'] = {
            'Signature': signature,
            'Version': version,
            'Size': size,
            'Flags': hex(flags),
            'TextMemoryOffset': hex(text_off),
            'TextSize': text_size,
            'RoMemoryOffset': hex(ro_off),
            'RoSize': ro_size,
            'DataMemoryOffset': hex(data_off),
            'DataSize': data_size,
            'BssSize': bss_size,
            'Reserved1': hex(reserved1),
            'ModuleId': ' '.join(f'{b:02x}' for b in module_id),
            'DsoHandleOffset': hex(dso_handle),
            'Reserved2': hex(reserved2),
            'EmbeddedOffset': hex(embed_off),
            'EmbeddedSize': embed_size,
            'DynStrOffset': hex(dynstr_off),
            'DynStrSize': dynstr_size,
            'DynSymOffset': hex(dynsym_off),
            'DynSymSize': dynsym_size
        }
        
        # Optional Assets
        if len(self.data) > size:
            offset = size
            asset_magic = self.data[offset:offset+4].decode('ascii')
            if asset_magic == 'ASET':
                self.has_assets = True
                offset += 4
                asset_version = struct.unpack_from('<I', self.data, offset)[0]
                offset += 4
                icon_off, icon_size, nacp_off, nacp_size, romfs_off, romfs_size = struct.unpack_from('<6Q', self.data, offset)
                self.properties['Assets'] = {
                    'Magic': asset_magic,
                    'Version': asset_version,
                    'IconOffset': hex(icon_off),
                    'IconSize': icon_size,
                    'NacpOffset': hex(nacp_off),
                    'NacpSize': nacp_size,
                    'RomfsOffset': hex(romfs_off),
                    'RomfsSize': romfs_size
                }
            else:
                print("No valid Assets Section")
        else:
            print("No Assets Section")

    def print_properties(self):
        for section, props in self.properties.items():
            print(f"{section}:")
            for key, value in props.items():
                print(f"  {key}: {value}")
    
    def write(self, new_filepath=None, updates=None):
        if not new_filepath:
            new_filepath = self.filepath + '.modified'
        data = bytearray(self.data)
        if updates:
            # Example: update version in NroHeader (offset 0x14)
            if 'Version' in updates:
                struct.pack_into('<I', data, 0x14, updates['Version'])
        with open(new_filepath, 'wb') as f:
            f.write(data)
        print(f"Written to {new_filepath}")

# Usage example
if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python nro_parser.py <path_to_nro>")
        sys.exit(1)
    parser = NROParser(sys.argv[1])
    parser.print_properties()
    # Example write: update version to 2 and save
    parser.write(updates={'Version': 2})

5. Java Class for .NRO Handling

This Java class uses ByteBuffer for decoding (little-endian). It can open a .NRO file, read/decode properties, print to console, and write modifications back.

import java.io.*;
import java.nio.*;
import java.nio.file.*;

public class NROParser {
    private Path filepath;
    private byte[] data;
    private java.util.Map<String, java.util.Map<String, Object>> properties = new java.util.HashMap<>();
    private boolean hasAssets = false;

    public NROParser(String filepath) throws IOException {
        this.filepath = Paths.get(filepath);
        this.data = Files.readAllBytes(this.filepath);
        read();
    }

    private void read() throws IOException {
        if (data.length < 0x80) {
            throw new IOException("Invalid .NRO file: too small");
        }
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int offset = 0;

        // RocrtHeader
        java.util.Map<String, Object> rocrt = new java.util.HashMap<>();
        byte[] moduleLoc = new byte[0xC];
        System.arraycopy(data, offset, moduleLoc, 0, 0xC);
        rocrt.put("ModuleHeaderLocation", bytesToHex(moduleLoc));
        offset += 0xC;
        rocrt.put("Reserved", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        properties.put("RocrtHeader", rocrt);

        // NroHeader
        java.util.Map<String, Object> nro = new java.util.HashMap<>();
        String signature = new String(data, offset, 4, "ASCII");
        if (!"NRO0".equals(signature)) {
            throw new IOException("Invalid signature");
        }
        nro.put("Signature", signature);
        offset += 4;
        nro.put("Version", bb.getInt(offset));
        offset += 4;
        int nroSize = bb.getInt(offset);
        nro.put("Size", nroSize);
        offset += 4;
        nro.put("Flags", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("TextMemoryOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("TextSize", bb.getInt(offset));
        offset += 4;
        nro.put("RoMemoryOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("RoSize", bb.getInt(offset));
        offset += 4;
        nro.put("DataMemoryOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("DataSize", bb.getInt(offset));
        offset += 4;
        nro.put("BssSize", bb.getInt(offset));
        offset += 4;
        nro.put("Reserved1", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        byte[] moduleId = new byte[32];
        System.arraycopy(data, offset, moduleId, 0, 32);
        nro.put("ModuleId", bytesToHex(moduleId));
        offset += 32;
        nro.put("DsoHandleOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("Reserved2", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("EmbeddedOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("EmbeddedSize", bb.getInt(offset));
        offset += 4;
        nro.put("DynStrOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("DynStrSize", bb.getInt(offset));
        offset += 4;
        nro.put("DynSymOffset", "0x" + Integer.toHexString(bb.getInt(offset)));
        offset += 4;
        nro.put("DynSymSize", bb.getInt(offset));
        properties.put("NroHeader", nro);

        // Optional Assets
        if (data.length > nroSize) {
            offset = nroSize;
            String assetMagic = new String(data, offset, 4, "ASCII");
            if ("ASET".equals(assetMagic)) {
                hasAssets = true;
                java.util.Map<String, Object> assets = new java.util.HashMap<>();
                assets.put("Magic", assetMagic);
                offset += 4;
                assets.put("Version", bb.getInt(offset));
                offset += 4;
                long iconOff = bb.getLong(offset);
                offset += 8;
                long iconSize = bb.getLong(offset);
                offset += 8;
                assets.put("IconOffset", "0x" + Long.toHexString(iconOff));
                assets.put("IconSize", iconSize);
                long nacpOff = bb.getLong(offset);
                offset += 8;
                long nacpSize = bb.getLong(offset);
                offset += 8;
                assets.put("NacpOffset", "0x" + Long.toHexString(nacpOff));
                assets.put("NacpSize", nacpSize);
                long romfsOff = bb.getLong(offset);
                offset += 8;
                long romfsSize = bb.getLong(offset);
                offset += 8;
                assets.put("RomfsOffset", "0x" + Long.toHexString(romfsOff));
                assets.put("RomfsSize", romfsSize);
                properties.put("Assets", assets);
            } else {
                System.out.println("No valid Assets Section");
            }
        } else {
            System.out.println("No Assets Section");
        }
    }

    public void printProperties() {
        for (java.util.Map.Entry<String, java.util.Map<String, Object>> section : properties.entrySet()) {
            System.out.println(section.getKey() + ":");
            for (java.util.Map.Entry<String, Object> prop : section.getValue().entrySet()) {
                System.out.println("  " + prop.getKey() + ": " + prop.getValue());
            }
        }
    }

    public void write(String newFilepath, java.util.Map<String, Object> updates) throws IOException {
        byte[] newData = data.clone();
        ByteBuffer bb = ByteBuffer.wrap(newData).order(ByteOrder.LITTLE_ENDIAN);
        if (updates != null) {
            // Example: update Version at offset 0x14
            if (updates.containsKey("Version")) {
                bb.putInt(0x14, (Integer) updates.get("Version"));
            }
        }
        Files.write(Paths.get(newFilepath != null ? newFilepath : filepath.toString() + ".modified"), newData);
        System.out.println("Written to " + (newFilepath != null ? newFilepath : filepath + ".modified"));
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x ", b));
        }
        return sb.toString().trim();
    }

    public static void main(String[] args) throws IOException {
        if (args.length < 1) {
            System.out.println("Usage: java NROParser <path_to_nro>");
            System.exit(1);
        }
        NROParser parser = new NROParser(args[0]);
        parser.printProperties();
        // Example write: update version to 2
        java.util.Map<String, Object> updates = new java.util.HashMap<>();
        updates.put("Version", 2);
        parser.write(null, updates);
    }
}

6. JavaScript Class for .NRO Handling (Node.js)

This Node.js class uses Buffer for decoding. It can open a .NRO file, read/decode properties, print to console, and write modifications back.

const fs = require('fs');

class NROParser {
  constructor(filepath) {
    this.filepath = filepath;
    this.data = null;
    this.properties = {};
    this.hasAssets = false;
    this.read();
  }

  read() {
    this.data = fs.readFileSync(this.filepath);
    if (this.data.length < 0x80) {
      throw new Error('Invalid .NRO file: too small');
    }
    let offset = 0;

    // RocrtHeader
    const moduleLoc = this.data.slice(offset, offset + 0xC).toString('hex').match(/.{1,2}/g).join(' ');
    offset += 0xC;
    const reserved = this.data.readUInt32LE(offset);
    offset += 4;
    this.properties.RocrtHeader = {
      ModuleHeaderLocation: moduleLoc,
      Reserved: `0x${reserved.toString(16)}`
    };

    // NroHeader
    const signature = this.data.slice(offset, offset + 4).toString('ascii');
    if (signature !== 'NRO0') {
      throw new Error('Invalid signature');
    }
    offset += 4;
    const version = this.data.readUInt32LE(offset);
    offset += 4;
    const nroSize = this.data.readUInt32LE(offset);
    offset += 4;
    const flags = this.data.readUInt32LE(offset);
    offset += 4;
    const textOff = this.data.readUInt32LE(offset);
    offset += 4;
    const textSize = this.data.readUInt32LE(offset);
    offset += 4;
    const roOff = this.data.readUInt32LE(offset);
    offset += 4;
    const roSize = this.data.readUInt32LE(offset);
    offset += 4;
    const dataOff = this.data.readUInt32LE(offset);
    offset += 4;
    const dataSize = this.data.readUInt32LE(offset);
    offset += 4;
    const bssSize = this.data.readUInt32LE(offset);
    offset += 4;
    const reserved1 = this.data.readUInt32LE(offset);
    offset += 4;
    const moduleId = this.data.slice(offset, offset + 32).toString('hex').match(/.{1,2}/g).join(' ');
    offset += 32;
    const dsoHandle = this.data.readUInt32LE(offset);
    offset += 4;
    const reserved2 = this.data.readUInt32LE(offset);
    offset += 4;
    const embedOff = this.data.readUInt32LE(offset);
    offset += 4;
    const embedSize = this.data.readUInt32LE(offset);
    offset += 4;
    const dynstrOff = this.data.readUInt32LE(offset);
    offset += 4;
    const dynstrSize = this.data.readUInt32LE(offset);
    offset += 4;
    const dynsymOff = this.data.readUInt32LE(offset);
    offset += 4;
    const dynsymSize = this.data.readUInt32LE(offset);
    this.properties.NroHeader = {
      Signature: signature,
      Version: version,
      Size: nroSize,
      Flags: `0x${flags.toString(16)}`,
      TextMemoryOffset: `0x${textOff.toString(16)}`,
      TextSize: textSize,
      RoMemoryOffset: `0x${roOff.toString(16)}`,
      RoSize: roSize,
      DataMemoryOffset: `0x${dataOff.toString(16)}`,
      DataSize: dataSize,
      BssSize: bssSize,
      Reserved1: `0x${reserved1.toString(16)}`,
      ModuleId: moduleId,
      DsoHandleOffset: `0x${dsoHandle.toString(16)}`,
      Reserved2: `0x${reserved2.toString(16)}`,
      EmbeddedOffset: `0x${embedOff.toString(16)}`,
      EmbeddedSize: embedSize,
      DynStrOffset: `0x${dynstrOff.toString(16)}`,
      DynStrSize: dynstrSize,
      DynSymOffset: `0x${dynsymOff.toString(16)}`,
      DynSymSize: dynsymSize
    };

    // Optional Assets
    if (this.data.length > nroSize) {
      offset = nroSize;
      const assetMagic = this.data.slice(offset, offset + 4).toString('ascii');
      if (assetMagic === 'ASET') {
        this.hasAssets = true;
        offset += 4;
        const assetVersion = this.data.readUInt32LE(offset);
        offset += 4;
        const iconOff = this.data.readBigUInt64LE(offset);
        offset += 8;
        const iconSize = this.data.readBigUInt64LE(offset);
        offset += 8;
        const nacpOff = this.data.readBigUInt64LE(offset);
        offset += 8;
        const nacpSize = this.data.readBigUInt64LE(offset);
        offset += 8;
        const romfsOff = this.data.readBigUInt64LE(offset);
        offset += 8;
        const romfsSize = this.data.readBigUInt64LE(offset);
        this.properties.Assets = {
          Magic: assetMagic,
          Version: assetVersion,
          IconOffset: `0x${iconOff.toString(16)}`,
          IconSize: Number(iconSize),
          NacpOffset: `0x${nacpOff.toString(16)}`,
          NacpSize: Number(nacpSize),
          RomfsOffset: `0x${romfsOff.toString(16)}`,
          RomfsSize: Number(romfsSize)
        };
      } else {
        console.log('No valid Assets Section');
      }
    } else {
      console.log('No Assets Section');
    }
  }

  printProperties() {
    for (const [section, props] of Object.entries(this.properties)) {
      console.log(`${section}:`);
      for (const [key, value] of Object.entries(props)) {
        console.log(`  ${key}: ${value}`);
      }
    }
  }

  write(newFilepath = null, updates = null) {
    let newData = Buffer.from(this.data);
    if (updates) {
      // Example: update Version at offset 0x14
      if ('Version' in updates) {
        newData.writeUInt32LE(updates.Version, 0x14);
      }
    }
    if (!newFilepath) newFilepath = this.filepath + '.modified';
    fs.writeFileSync(newFilepath, newData);
    console.log(`Written to ${newFilepath}`);
  }
}

// Usage example
if (process.argv.length < 3) {
  console.log('Usage: node nro_parser.js <path_to_nro>');
  process.exit(1);
}
const parser = new NROParser(process.argv[2]);
parser.printProperties();
// Example write: update version to 2
parser.write(null, { Version: 2 });

7. C "Class" for .NRO Handling

C doesn't have classes, so this uses a struct with functions. It can open a .NRO file, read/decode properties, print to console, and write modifications back. Compile with gcc nro_parser.c -o nro_parser.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <byteswap.h> // For big-endian systems, but assuming little-endian host

typedef struct {
    char* filepath;
    uint8_t* data;
    size_t size;
    // Properties storage (use simple arrays/maps simulation)
    char rocrt_module_loc[37]; // Hex string
    uint32_t rocrt_reserved;
    char nro_signature[5];
    uint32_t nro_version;
    uint32_t nro_size;
    uint32_t nro_flags;
    uint32_t nro_text_off;
    uint32_t nro_text_size;
    uint32_t nro_ro_off;
    uint32_t nro_ro_size;
    uint32_t nro_data_off;
    uint32_t nro_data_size;
    uint32_t nro_bss_size;
    uint32_t nro_reserved1;
    char nro_module_id[97]; // 32*3 -1
    uint32_t nro_dso_handle;
    uint32_t nro_reserved2;
    uint32_t nro_embed_off;
    uint32_t nro_embed_size;
    uint32_t nro_dynstr_off;
    uint32_t nro_dynstr_size;
    uint32_t nro_dynsym_off;
    uint32_t nro_dynsym_size;
    int has_assets;
    char assets_magic[5];
    uint32_t assets_version;
    uint64_t assets_icon_off;
    uint64_t assets_icon_size;
    uint64_t assets_nacp_off;
    uint64_t assets_nacp_size;
    uint64_t assets_romfs_off;
    uint64_t assets_romfs_size;
} NROParser;

void init_nro_parser(NROParser* parser, const char* filepath) {
    parser->filepath = strdup(filepath);
    FILE* f = fopen(filepath, "rb");
    if (!f) {
        perror("Failed to open file");
        exit(1);
    }
    fseek(f, 0, SEEK_END);
    parser->size = ftell(f);
    fseek(f, 0, SEEK_SET);
    parser->data = malloc(parser->size);
    fread(parser->data, 1, parser->size, f);
    fclose(f);
    parser->has_assets = 0;
}

void read_nro(NROParser* parser) {
    if (parser->size < 0x80) {
        fprintf(stderr, "Invalid .NRO file: too small\n");
        exit(1);
    }
    uint8_t* data = parser->data;
    size_t offset = 0;

    // RocrtHeader
    for (int i = 0; i < 0xC; i++) {
        char hex[4];
        sprintf(hex, "%02x ", data[offset + i]);
        strcat(parser->rocrt_module_loc, hex);
    }
    parser->rocrt_module_loc[strlen(parser->rocrt_module_loc) - 1] = '\0'; // Trim space
    offset += 0xC;
    memcpy(&parser->rocrt_reserved, data + offset, 4);
    offset += 4;

    // NroHeader
    memcpy(parser->nro_signature, data + offset, 4);
    parser->nro_signature[4] = '\0';
    if (strcmp(parser->nro_signature, "NRO0") != 0) {
        fprintf(stderr, "Invalid signature\n");
        exit(1);
    }
    offset += 4;
    memcpy(&parser->nro_version, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_flags, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_text_off, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_text_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_ro_off, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_ro_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_data_off, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_data_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_bss_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_reserved1, data + offset, 4);
    offset += 4;
    for (int i = 0; i < 32; i++) {
        char hex[4];
        sprintf(hex, "%02x ", data[offset + i]);
        strcat(parser->nro_module_id, hex);
    }
    parser->nro_module_id[strlen(parser->nro_module_id) - 1] = '\0';
    offset += 32;
    memcpy(&parser->nro_dso_handle, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_reserved2, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_embed_off, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_embed_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_dynstr_off, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_dynstr_size, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_dynsym_off, data + offset, 4);
    offset += 4;
    memcpy(&parser->nro_dynsym_size, data + offset, 4);

    // Optional Assets
    if (parser->size > parser->nro_size) {
        offset = parser->nro_size;
        memcpy(parser->assets_magic, data + offset, 4);
        parser->assets_magic[4] = '\0';
        if (strcmp(parser->assets_magic, "ASET") == 0) {
            parser->has_assets = 1;
            offset += 4;
            memcpy(&parser->assets_version, data + offset, 4);
            offset += 4;
            memcpy(&parser->assets_icon_off, data + offset, 8);
            offset += 8;
            memcpy(&parser->assets_icon_size, data + offset, 8);
            offset += 8;
            memcpy(&parser->assets_nacp_off, data + offset, 8);
            offset += 8;
            memcpy(&parser->assets_nacp_size, data + offset, 8);
            offset += 8;
            memcpy(&parser->assets_romfs_off, data + offset, 8);
            offset += 8;
            memcpy(&parser->assets_romfs_size, data + offset, 8);
        } else {
            printf("No valid Assets Section\n");
        }
    } else {
        printf("No Assets Section\n");
    }
}

void print_nro_properties(const NROParser* parser) {
    printf("RocrtHeader:\n");
    printf("  ModuleHeaderLocation: %s\n", parser->rocrt_module_loc);
    printf("  Reserved: 0x%x\n", parser->rocrt_reserved);
    
    printf("NroHeader:\n");
    printf("  Signature: %s\n", parser->nro_signature);
    printf("  Version: %u\n", parser->nro_version);
    printf("  Size: %u\n", parser->nro_size);
    printf("  Flags: 0x%x\n", parser->nro_flags);
    printf("  TextMemoryOffset: 0x%x\n", parser->nro_text_off);
    printf("  TextSize: %u\n", parser->nro_text_size);
    printf("  RoMemoryOffset: 0x%x\n", parser->nro_ro_off);
    printf("  RoSize: %u\n", parser->nro_ro_size);
    printf("  DataMemoryOffset: 0x%x\n", parser->nro_data_off);
    printf("  DataSize: %u\n", parser->nro_data_size);
    printf("  BssSize: %u\n", parser->nro_bss_size);
    printf("  Reserved1: 0x%x\n", parser->nro_reserved1);
    printf("  ModuleId: %s\n", parser->nro_module_id);
    printf("  DsoHandleOffset: 0x%x\n", parser->nro_dso_handle);
    printf("  Reserved2: 0x%x\n", parser->nro_reserved2);
    printf("  EmbeddedOffset: 0x%x\n", parser->nro_embed_off);
    printf("  EmbeddedSize: %u\n", parser->nro_embed_size);
    printf("  DynStrOffset: 0x%x\n", parser->nro_dynstr_off);
    printf("  DynStrSize: %u\n", parser->nro_dynstr_size);
    printf("  DynSymOffset: 0x%x\n", parser->nro_dynsym_off);
    printf("  DynSymSize: %u\n", parser->nro_dynsym_size);
    
    if (parser->has_assets) {
        printf("Assets:\n");
        printf("  Magic: %s\n", parser->assets_magic);
        printf("  Version: %u\n", parser->assets_version);
        printf("  IconOffset: 0x%llx\n", (unsigned long long)parser->assets_icon_off);
        printf("  IconSize: %llu\n", (unsigned long long)parser->assets_icon_size);
        printf("  NacpOffset: 0x%llx\n", (unsigned long long)parser->assets_nacp_off);
        printf("  NacpSize: %llu\n", (unsigned long long)parser->assets_nacp_size);
        printf("  RomfsOffset: 0x%llx\n", (unsigned long long)parser->assets_romfs_off);
        printf("  RomfsSize: %llu\n", (unsigned long long)parser->assets_romfs_size);
    }
}

void write_nro(const NROParser* parser, const char* new_filepath, uint32_t new_version) {
    uint8_t* new_data = malloc(parser->size);
    memcpy(new_data, parser->data, parser->size);
    // Example update: version at 0x14
    memcpy(new_data + 0x14, &new_version, 4);
    
    char* out_path = (char*)new_filepath;
    if (!out_path) {
        out_path = malloc(strlen(parser->filepath) + 10);
        sprintf(out_path, "%s.modified", parser->filepath);
    }
    FILE* f = fopen(out_path, "wb");
    fwrite(new_data, 1, parser->size, f);
    fclose(f);
    printf("Written to %s\n", out_path);
    free(new_data);
    if (!new_filepath) free(out_path);
}

void free_nro_parser(NROParser* parser) {
    free(parser->filepath);
    free(parser->data);
}

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: %s <path_to_nro>\n", argv[0]);
        return 1;
    }
    NROParser parser;
    memset(&parser, 0, sizeof(parser));
    init_nro_parser(&parser, argv[1]);
    read_nro(&parser);
    print_nro_properties(&parser);
    // Example write: set version to 2
    write_nro(&parser, NULL, 2);
    free_nro_parser(&parser);
    return 0;
}