Task 002: .3DSX File Format

Task 002: .3DSX File Format

The .3DSX file format is an executable file format designed for homebrew applications on the Nintendo 3DS. It is a relocatable, statically-linked executable format derived from the ELF standard, tailored for the 3DS’s 32-bit ARM architecture. Below, I address each part of your request based on the specifications available from reliable sources, primarily the 3DBrew documentation.


1. Properties of the .3DSX File Format

The .3DSX file format has a structured layout with specific properties intrinsic to its file system. Based on the 3DBrew specification, the following properties define the format:

Header (0x20 bytes or more, depending on extended header):

  • Magic (0x0, 4 bytes): The identifier "3DSX" (ASCII).
  • Header Size (0x4, 2 bytes): Size of the header, typically 0x20 (32 bytes) unless an extended header is present.
  • Relocation Header Size (0x6, 2 bytes): Size of each relocation table header (usually 8 bytes).
  • Format Version (0x8, 4 bytes): Version of the .3DSX format.
  • Flags (0xC, 4 bytes): Configuration flags for the executable.
  • Code Segment Size (0x10, 4 bytes): Size of the code segment in bytes.
  • Rodata Segment Size (0x14, 4 bytes): Size of the read-only data (rodata) segment in bytes.
  • Data Segment Size (0x18, 4 bytes): Size of the data segment, including BSS (uninitialized data).
  • BSS Segment Size (0x1C, 4 bytes): Size of the BSS segment (uninitialized data).

Extended Header (if Header Size > 0x20):

  • SMDH Offset (0x0, 4 bytes): Offset to the SMDH (icon/metadata) section.
  • SMDH Size (0x4, 4 bytes): Size of the SMDH section.
  • RomFS Offset (0x8, 4 bytes): Offset to the RomFS (file system) data, if present.

Relocation Table Headers (Code, Rodata, Data):

  • Number of Absolute Relocations (0x0, 4 bytes): Count of absolute relocations for the segment.
  • Number of Relative Relocations (0x4, 4 bytes): Count of relative relocations for the segment.

Relocation Tables:

  • Number of Words to Skip (0x0, 2 bytes): Number of words to skip before patching.
  • Number of Words to Patch (0x2, 2 bytes): Number of words to apply relocation patches to.

Segments:

  • Code Segment: Contains executable ARM code.
  • Rodata Segment: Contains read-only data.
  • Data Segment: Contains initialized data and BSS (uninitialized data).
  • SMDH Data (optional): Metadata, including the application icon and description.
  • RomFS Data (optional): Embedded file system for resources.

Additional Properties:

  • Relocatable Nature: The format supports relocatable code, allowing execution at varying memory addresses.
  • No Shared Libraries: Only statically-linked executables are supported.
  • No Debugging Support: The format lacks debugging features.
  • ZIP Archive Structure: .3DSX files are essentially ZIP archives containing executable code, resources, and metadata, though the executable structure follows the above layout.

2. Python Class for .3DSX File Handling

import struct

class ThreeDSXFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}

    def read_3dsx(self):
        with open(self.filepath, 'rb') as f:
            # Read Header
            header = f.read(0x20)
            if len(header) < 0x20:
                raise ValueError("File too small to be a valid .3DSX")
            
            self.properties['magic'] = header[0:4].decode('ascii')
            self.properties['header_size'] = struct.unpack('<H', header[4:6])[0]
            self.properties['reloc_hdr_size'] = struct.unpack('<H', header[6:8])[0]
            self.properties['format_version'] = struct.unpack('<I', header[8:12])[0]
            self.properties['flags'] = struct.unpack('<I', header[12:16])[0]
            self.properties['code_size'] = struct.unpack('<I', header[16:20])[0]
            self.properties['rodata_size'] = struct.unpack('<I', header[20:24])[0]
            self.properties['data_size'] = struct.unpack('<I', header[24:28])[0]
            self.properties['bss_size'] = struct.unpack('<I', header[28:32])[0]

            # Read Extended Header if present
            if self.properties['header_size'] > 0x20:
                ext_header = f.read(self.properties['header_size'] - 0x20)
                self.properties['smdh_offset'] = struct.unpack('<I', ext_header[0:4])[0]
                self.properties['smdh_size'] = struct.unpack('<I', ext_header[4:8])[0]
                self.properties['romfs_offset'] = struct.unpack('<I', ext_header[8:12])[0]
            else:
                self.properties['smdh_offset'] = 0
                self.properties['smdh_size'] = 0
                self.properties['romfs_offset'] = 0

            # Read Relocation Table Headers
            for segment in ['code', 'rodata', 'data']:
                reloc_hdr = f.read(8)
                self.properties[f'{segment}_abs_relocs'] = struct.unpack('<I', reloc_hdr[0:4])[0]
                self.properties[f'{segment}_rel_relocs'] = struct.unpack('<I', reloc_hdr[4:8])[0]

            # Skip segments and relocation tables (for simplicity, not parsing content)
            f.seek(self.properties['code_size'] + self.properties['rodata_size'] + self.properties['data_size'], 1)

    def write_3dsx(self, output_filepath):
        with open(output_filepath, 'wb') as f:
            # Write Header
            f.write(struct.pack(
                '<4sHHIII',
                self.properties['magic'].encode('ascii'),
                self.properties['header_size'],
                self.properties['reloc_hdr_size'],
                self.properties['format_version'],
                self.properties['flags'],
                self.properties['code_size'],
                self.properties['rodata_size'],
                self.properties['data_size'],
                self.properties['bss_size']
            ))

            # Write Extended Header if present
            if self.properties['header_size'] > 0x20:
                f.write(struct.pack(
                    '<III',
                    self.properties['smdh_offset'],
                    self.properties['smdh_size'],
                    self.properties['romfs_offset']
                ))

            # Write Relocation Table Headers
            for segment in ['code', 'rodata', 'data']:
                f.write(struct.pack(
                    '<II',
                    self.properties[f'{segment}_abs_relocs'],
                    self.properties[f'{segment}_rel_relocs']
                ))

            # Note: Actual segment and relocation table data would need to be preserved and written here

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

# Example Usage
if __name__ == "__main__":
    try:
        dsx = ThreeDSXFile("boot.3dsx")
        dsx.read_3dsx()
        dsx.print_properties()
        dsx.write_3dsx("output.3dsx")
    except Exception as e:
        print(f"Error: {e}")

Notes:

  • This Python class reads the .3DSX header, extended header, and relocation table headers, storing properties in a dictionary.
  • The write_3dsx method writes a new .3DSX file with the same header structure but skips segment and relocation table content for simplicity (as they require complex parsing).
  • The class assumes the file is valid and does not handle segment data or relocations fully, as this would require additional logic to parse and reconstruct the executable content.

3. Java Class for .3DSX File Handling

import java.io.*;

public class ThreeDSXFile {
    private String filepath;
    private java.util.Map<String, Object> properties;

    public ThreeDSXFile(String filepath) {
        this.filepath = filepath;
        this.properties = new java.util.HashMap<>();
    }

    public void read3DSX() throws IOException {
        try (DataInputStream dis = new DataInputStream(new FileInputStream(filepath))) {
            // Read Header
            byte[] header = new byte[0x20];
            if (dis.read(header) < 0x20) {
                throw new IOException("File too small to be a valid .3DSX");
            }

            properties.put("magic", new String(header, 0, 4, "ASCII"));
            properties.put("header_size", readShortLE(dis, header, 4));
            properties.put("reloc_hdr_size", readShortLE(dis, header, 6));
            properties.put("format_version", readIntLE(dis, header, 8));
            properties.put("flags", readIntLE(dis, header, 12));
            properties.put("code_size", readIntLE(dis, header, 16));
            properties.put("rodata_size", readIntLE(dis, header, 20));
            properties.put("data_size", readIntLE(dis, header, 24));
            properties.put("bss_size", readIntLE(dis, header, 28));

            // Read Extended Header
            if ((int) properties.get("header_size") > 0x20) {
                byte[] extHeader = new byte[(int) properties.get("header_size") - 0x20];
                dis.read(extHeader);
                properties.put("smdh_offset", readIntLE(dis, extHeader, 0));
                properties.put("smdh_size", readIntLE(dis, extHeader, 4));
                properties.put("romfs_offset", readIntLE(dis, extHeader, 8));
            } else {
                properties.put("smdh_offset", 0);
                properties.put("smdh_size", 0);
                properties.put("romfs_offset", 0);
            }

            // Read Relocation Table Headers
            for (String segment : new String[]{"code", "rodata", "data"}) {
                byte[] relocHdr = new byte[8];
                dis.read(relocHdr);
                properties.put(segment + "_abs_relocs", readIntLE(dis, relocHdr, 0));
                properties.put(segment + "_rel_relocs", readIntLE(dis, relocHdr, 4));
            }

            // Skip segments
            dis.skipBytes((int) properties.get("code_size") + (int) properties.get("rodata_size") + (int) properties.get("data_size"));
        }
    }

    private int readShortLE(DataInputStream dis, byte[] data, int offset) {
        return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
    }

    private int readIntLE(DataInputStream dis, byte[] data, int offset) {
        return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) |
               ((data[offset + 2] & 0xFF) << 16) | ((data[offset + 3] & 0xFF) << 24);
    }

    public void write3DSX(String outputFilepath) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(outputFilepath))) {
            // Write Header
            dos.writeBytes((String) properties.get("magic"));
            dos.writeShort(Integer.reverseBytes((int) properties.get("header_size")) >> 16);
            dos.writeShort(Integer.reverseBytes((int) properties.get("reloc_hdr_size")) >> 16);
            dos.writeInt(Integer.reverseBytes((int) properties.get("format_version")));
            dos.writeInt(Integer.reverseBytes((int) properties.get("flags")));
            dos.writeInt(Integer.reverseBytes((int) properties.get("code_size")));
            dos.writeInt(Integer.reverseBytes((int) properties.get("rodata_size")));
            dos.writeInt(Integer.reverseBytes((int) properties.get("data_size")));
            dos.writeInt(Integer.reverseBytes((int) properties.get("bss_size")));

            // Write Extended Header
            if ((int) properties.get("header_size") > 0x20) {
                dos.writeInt(Integer.reverseBytes((int) properties.get("smdh_offset")));
                dos.writeInt(Integer.reverseBytes((int) properties.get("smdh_size")));
                dos.writeInt(Integer.reverseBytes((int) properties.get("romfs_offset")));
            }

            // Write Relocation Table Headers
            for (String segment : new String[]{"code", "rodata", "data"}) {
                dos.writeInt(Integer.reverseBytes((int) properties.get(segment + "_abs_relocs")));
                dos.writeInt(Integer.reverseBytes((int) properties.get(segment + "_rel_relocs")));
            }
        }
    }

    public void printProperties() {
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
    }

    public static void main(String[] args) {
        try {
            ThreeDSXFile dsx = new ThreeDSXFile("boot.3dsx");
            dsx.read3DSX();
            dsx.printProperties();
            dsx.write3DSX("output.3dsx");
        } catch (IOException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

Notes:

  • The Java class uses DataInputStream and DataOutputStream for binary I/O, handling little-endian data.
  • Custom methods readShortLE and readIntLE handle little-endian byte order.
  • Segment and relocation table content is skipped for simplicity, as in the Python implementation.

4. JavaScript Class for .3DSX File Handling

const fs = require('fs');

class ThreeDSXFile {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = {};
    }

    read3DSX() {
        const buffer = fs.readFileSync(this.filepath);
        if (buffer.length < 0x20) {
            throw new Error('File too small to be a valid .3DSX');
        }

        // Read Header
        this.properties.magic = buffer.toString('ascii', 0, 4);
        this.properties.header_size = buffer.readUInt16LE(4);
        this.properties.reloc_hdr_size = buffer.readUInt16LE(6);
        this.properties.format_version = buffer.readUInt32LE(8);
        this.properties.flags = buffer.readUInt32LE(12);
        this.properties.code_size = buffer.readUInt32LE(16);
        this.properties.rodata_size = buffer.readUInt32LE(20);
        this.properties.data_size = buffer.readUInt32LE(24);
        this.properties.bss_size = buffer.readUInt32LE(28);

        // Read Extended Header
        let offset = 0x20;
        if (this.properties.header_size > 0x20) {
            this.properties.smdh_offset = buffer.readUInt32LE(offset);
            this.properties.smdh_size = buffer.readUInt32LE(offset + 4);
            this.properties.romfs_offset = buffer.readUInt32LE(offset + 8);
            offset += this.properties.header_size - 0x20;
        } else {
            this.properties.smdh_offset = 0;
            this.properties.smdh_size = 0;
            this.properties.romfs_offset = 0;
        }

        // Read Relocation Table Headers
        for (let segment of ['code', 'rodata', 'data']) {
            this.properties[`${segment}_abs_relocs`] = buffer.readUInt32LE(offset);
            this.properties[`${segment}_rel_relocs`] = buffer.readUInt32LE(offset + 4);
            offset += 8;
        }
    }

    write3DSX(outputFilepath) {
        const buffer = Buffer.alloc(this.properties.header_size + 24);
        let offset = 0;

        // Write Header
        buffer.write(this.properties.magic, 0, 4, 'ascii'); offset += 4;
        buffer.writeUInt16LE(this.properties.header_size, offset); offset += 2;
        buffer.writeUInt16LE(this.properties.reloc_hdr_size, offset); offset += 2;
        buffer.writeUInt32LE(this.properties.format_version, offset); offset += 4;
        buffer.writeUInt32LE(this.properties.flags, offset); offset += 4;
        buffer.writeUInt32LE(this.properties.code_size, offset); offset += 4;
        buffer.writeUInt32LE(this.properties.rodata_size, offset); offset += 4;
        buffer.writeUInt32LE(this.properties.data_size, offset); offset += 4;
        buffer.writeUInt32LE(this.properties.bss_size, offset); offset += 4;

        // Write Extended Header
        if (this.properties.header_size > 0x20) {
            buffer.writeUInt32LE(this.properties.smdh_offset, offset); offset += 4;
            buffer.writeUInt32LE(this.properties.smdh_size, offset); offset += 4;
            buffer.writeUInt32LE(this.properties.romfs_offset, offset); offset += 4;
        }

        // Write Relocation Table Headers
        for (let segment of ['code', 'rodata', 'data']) {
            buffer.writeUInt32LE(this.properties[`${segment}_abs_relocs`], offset); offset += 4;
            buffer.writeUInt32LE(this.properties[`${segment}_rel_relocs`], offset); offset += 4;
        }

        fs.writeFileSync(outputFilepath, buffer);
    }

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

// Example Usage
try {
    const dsx = new ThreeDSXFile('boot.3dsx');
    dsx.read3DSX();
    dsx.printProperties();
    dsx.write3DSX('output.3dsx');
} catch (e) {
    console.error(`Error: ${e.message}`);
}

Notes:

  • This JavaScript class uses Node.js’s fs module for file I/O.
  • It handles binary data using Buffer, with methods like readUInt16LE for little-endian parsing.
  • The write3DSX method creates a minimal file with headers but omits segment data, as in previous implementations.

5. C Class for .3DSX File Handling

Since C does not have a direct equivalent to a "class," I’ll provide a C structure and associated functions to achieve the same functionality.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char magic[4];
    unsigned short header_size;
    unsigned short reloc_hdr_size;
    unsigned int format_version;
    unsigned int flags;
    unsigned int code_size;
    unsigned int rodata_size;
    unsigned int data_size;
    unsigned int bss_size;
    unsigned int smdh_offset;
    unsigned int smdh_size;
    unsigned int romfs_offset;
    unsigned int code_abs_relocs;
    unsigned int code_rel_relocs;
    unsigned int rodata_abs_relocs;
    unsigned int rodata_rel_relocs;
    unsigned int data_abs_relocs;
    unsigned int data_rel_relocs;
} ThreeDSXFile;

void read_3dsx(const char* filepath, ThreeDSXFile* dsx) {
    FILE* f = fopen(filepath, "rb");
    if (!f) {
        fprintf(stderr, "Error: Cannot open file %s\n", filepath);
        exit(1);
    }

    // Read Header
    unsigned char header[0x20];
    if (fread(header, 1, 0x20, f) != 0x20) {
        fprintf(stderr, "Error: File too small to be a valid .3DSX\n");
        fclose(f);
        exit(1);
    }

    memcpy(dsx->magic, header, 4);
    dsx->header_size = header[4] | (header[5] << 8);
    dsx->reloc_hdr_size = header[6] | (header[7] << 8);
    dsx->format_version = header[8] | (header[9] << 8) | (header[10] << 16) | (header[11] << 24);
    dsx->flags = header[12] | (header[13] << 8) | (header[14] << 16) | (header[15] << 24);
    dsx->code_size = header[16] | (header[17] << 8) | (header[18] << 16) | (header[19] << 24);
    dsx->rodata_size = header[20] | (header[21] << 8) | (header[22] << 16) | (header[23] << 24);
    dsx->data_size = header[24] | (header[25] << 8) | (header[26] << 16) | (header[27] << 24);
    dsx->bss_size = header[28] | (header[29] << 8) | (header[30] << 16) | (header[31] << 24);

    // Read Extended Header
    if (dsx->header_size > 0x20) {
        unsigned char* ext_header = malloc(dsx->header_size - 0x20);
        fread(ext_header, 1, dsx->header_size - 0x20, f);
        dsx->smdh_offset = ext_header[0] | (ext_header[1] << 8) | (ext_header[2] << 16) | (ext_header[3] << 24);
        dsx->smdh_size = ext_header[4] | (ext_header[5] << 8) | (ext_header[6] << 16) | (ext_header[7] << 24);
        dsx->romfs_offset = ext_header[8] | (ext_header[9] << 8) | (ext_header[10] << 16) | (ext_header[11] << 24);
        free(ext_header);
    } else {
        dsx->smdh_offset = 0;
        dsx->smdh_size = 0;
        dsx->romfs_offset = 0;
    }

    // Read Relocation Table Headers
    unsigned char reloc_hdr[8];
    for (int i = 0; i < 3; i++) {
        fread(reloc_hdr, 1, 8, f);
        unsigned int abs = reloc_hdr[0] | (reloc_hdr[1] << 8) | (reloc_hdr[2] << 16) | (reloc_hdr[3] << 24);
        unsigned int rel = reloc_hdr[4] | (reloc_hdr[5] << 8) | (reloc_hdr[6] << 16) | (reloc_hdr[7] << 24);
        if (i == 0) {
            dsx->code_abs_relocs = abs;
            dsx->code_rel_relocs = rel;
        } else if (i == 1) {
            dsx->rodata_abs_relocs = abs;
            dsx->rodata_rel_relocs = rel;
        } else {
            dsx->data_abs_relocs = abs;
            dsx->data_rel_relocs = rel;
        }
    }

    fseek(f, dsx->code_size + dsx->rodata_size + dsx->data_size, SEEK_CUR);
    fclose(f);
}

void write_3dsx(const char* output_filepath, ThreeDSXFile* dsx) {
    FILE* f = fopen(output_filepath, "wb");
    if (!f) {
        fprintf(stderr, "Error: Cannot create file %s\n", output_filepath);
        exit(1);
    }

    // Write Header
    fwrite(dsx->magic, 1, 4, f);
    fwrite(&dsx->header_size, 2, 1, f);
    fwrite(&dsx->reloc_hdr_size, 2, 1, f);
    fwrite(&dsx->format_version, 4, 1, f);
    fwrite(&dsx->flags, 4, 1, f);
    fwrite(&dsx->code_size, 4, 1, f);
    fwrite(&dsx->rodata_size, 4, 1, f);
    fwrite(&dsx->data_size, 4, 1, f);
    fwrite(&dsx->bss_size, 4, 1, f);

    // Write Extended Header
    if (dsx->header_size > 0x20) {
        fwrite(&dsx->smdh_offset, 4, 1, f);
        fwrite(&dsx->smdh_size, 4, 1, f);
        fwrite(&dsx->romfs_offset, 4, 1, f);
    }

    // Write Relocation Table Headers
    unsigned int relocs[6] = {
        dsx->code_abs_relocs, dsx->code_rel_relocs,
        dsx->rodata_abs_relocs, dsx->rodata_rel_relocs,
        dsx->data_abs_relocs, dsx->data_rel_relocs
    };
    fwrite(relocs, 4, 6, f);

    fclose(f);
}

void print_properties(ThreeDSXFile* dsx) {
    printf("magic: %.4s\n", dsx->magic);
    printf("header_size: %u\n", dsx->header_size);
    printf("reloc_hdr_size: %u\n", dsx->reloc_hdr_size);
    printf("format_version: %u\n", dsx->format_version);
    printf("flags: %u\n", dsx->flags);
    printf("code_size: %u\n", dsx->code_size);
    printf("rodata_size: %u\n", dsx->rodata_size);
    printf("data_size: %u\n", dsx->data_size);
    printf("bss_size: %u\n", dsx->bss_size);
    printf("smdh_offset: %u\n", dsx->smdh_offset);
    printf("smdh_size: %u\n", dsx->smdh_size);
    printf("romfs_offset: %u\n", dsx->romfs_offset);
    printf("code_abs_relocs: %u\n", dsx->code_abs_relocs);
    printf("code_rel_relocs: %u\n", dsx->code_rel_relocs);
    printf("rodata_abs_relocs: %u\n", dsx->rodata_abs_relocs);
    printf("rodata_rel_relocs: %u\n", dsx->rodata_rel_relocs);
    printf("data_abs_relocs: %u\n", dsx->data_abs_relocs);
    printf("data_rel_relocs: %u\n", dsx->data_rel_relocs);
}

int main() {
    ThreeDSXFile dsx = {0};
    read_3dsx("boot.3dsx", &dsx);
    print_properties(&dsx);
    write_3dsx("output.3dsx", &dsx);
    return 0;
}

Notes:

  • The C implementation uses a struct to store properties and functions for reading, writing, and printing.
  • It handles little-endian byte order manually, as C lacks built-in endian conversion functions.
  • Memory management is explicit, with dynamic allocation for the extended header.

General Notes and Limitations

  • Incomplete Segment Handling: The implementations focus on parsing and writing headers and relocation table headers, as parsing the full content of code, rodata, data, SMDH, and RomFS sections requires additional logic specific to the application’s needs (e.g., ARM instruction decoding, RomFS parsing).
  • File Access: The classes assume the .3DSX file is accessible on the local file system. For actual 3DS execution, files must be placed on an SD card in the 3ds folder or root (for boot.3dsx).
  • Error Handling: Basic error checking is included, but robust validation (e.g., magic number verification, segment bounds checking) should be added for production use.
  • Relocation Tables: The relocation tables themselves are not fully parsed or written, as they involve complex patching logic that depends on the executable’s memory layout.
  • ZIP Archive Consideration: While .3DSX files are described as ZIP archives in some sources, the executable structure is the primary focus here, as the ZIP container is typically handled by the homebrew launcher.

If you need more detailed parsing of segments, relocation tables, or RomFS/SMDH data, please let me know, and I can extend the implementations accordingly.

1. List of Properties of the .3DSX File Format

The .3DSX file format is a relocatable executable format used for Nintendo 3DS homebrew applications. It includes a header, optional extended header, relocation headers for segments, segment data (code, rodata, data), relocation tables, and optional SMDH (icon/metadata) and RomFS (file system) sections. The properties intrinsic to the format (fields, segments, and sections) are as follows:

  • Magic: 4-byte string, always "3DSX".
  • Header Size: 16-bit unsigned integer (u16), typically 0x20 (32 bytes) for basic header or 0x2C (44 bytes) for extended header.
  • Relocation Header Size: u16, size of each relocation header (typically 0x08 or 8 bytes).
  • Format Version: 32-bit unsigned integer (u32), typically 0.
  • Flags: u32, typically 0 (reserved or unused).
  • Code Segment Size: u32, size of the code segment in bytes (must be multiple of 0x1000 for page alignment).
  • Rodata Segment Size: u32, size of the read-only data segment in bytes (must be multiple of 0x1000).
  • Data Segment Size: u32, total size of the data segment in bytes, including BSS (must be multiple of 0x1000).
  • BSS Size: u32, size of the uninitialized BSS portion of the data segment in bytes (not stored in file).
  • SMDH Offset: u32 (present if header size > 0x20), absolute file offset to SMDH data (0 if absent).
  • SMDH Size: u32 (present if header size > 0x20), size of SMDH data in bytes (0 if absent).
  • RomFS Offset: u32 (present if header size > 0x20), absolute file offset to RomFS data (0 if absent; RomFS size is implicit, from offset to end of file).
  • Code Absolute Relocation Count: u32, number of absolute relocations for the code segment.
  • Code Relative Relocation Count: u32, number of relative relocations for the code segment (typically 0).
  • Rodata Absolute Relocation Count: u32, number of absolute relocations for the rodata segment.
  • Rodata Relative Relocation Count: u32, number of relative relocations for the rodata segment (typically 0).
  • Data Absolute Relocation Count: u32, number of absolute relocations for the data segment.
  • Data Relative Relocation Count: u32, number of relative relocations for the data segment (typically 0).
  • Code Segment Data: Byte array of size equal to Code Segment Size.
  • Rodata Segment Data: Byte array of size equal to Rodata Segment Size.
  • Data Segment Data: Byte array of size equal to Data Segment Size minus BSS Size (initialized data only).
  • Code Absolute Relocations: List of pairs (u16 skip, u16 patch), where sum of patch values equals Code Absolute Relocation Count.
  • Code Relative Relocations: List of pairs (u16 skip, u16 patch), where sum of patch values equals Code Relative Relocation Count (typically empty).
  • Rodata Absolute Relocations: List of pairs (u16 skip, u16 patch), where sum of patch values equals Rodata Absolute Relocation Count.
  • Rodata Relative Relocations: List of pairs (u16 skip, u16 patch), where sum of patch values equals Rodata Relative Relocation Count (typically empty).
  • Data Absolute Relocations: List of pairs (u16 skip, u16 patch), where sum of patch values equals Data Absolute Relocation Count.
  • Data Relative Relocations: List of pairs (u16 skip, u16 patch), where sum of patch values equals Data Relative Relocation Count (typically empty).
  • SMDH Data: Byte array of size equal to SMDH Size (if present).
  • RomFS Data: Byte array from RomFS Offset to end of file (if present).

These properties define the file's structure, with segments page-aligned and relocations using a compact skip-patch encoding for efficiency.

2. Python Class

import struct
import os

class ThreeDSXFile:
    def __init__(self, filepath=None):
        self.magic = b'3DSX'
        self.header_size = 0x20
        self.reloc_hdr_size = 0x08
        self.format_version = 0
        self.flags = 0
        self.code_size = 0
        self.rodata_size = 0
        self.data_size = 0
        self.bss_size = 0
        self.has_extended = False
        self.smdh_offset = 0
        self.smdh_size = 0
        self.romfs_offset = 0
        self.code_abs_count = 0
        self.code_rel_count = 0
        self.rodata_abs_count = 0
        self.rodata_rel_count = 0
        self.data_abs_count = 0
        self.data_rel_count = 0
        self.code_data = b''
        self.rodata_data = b''
        self.data_data = b''
        self.code_abs_relocs = []
        self.code_rel_relocs = []
        self.rodata_abs_relocs = []
        self.rodata_rel_relocs = []
        self.data_abs_relocs = []
        self.data_rel_relocs = []
        self.smdh_data = b''
        self.romfs_data = b''
        if filepath:
            self.read(filepath)

    def read_relocs(self, f, count):
        relocs = []
        total_patched = 0
        while total_patched < count:
            skip, patch = struct.unpack('<HH', f.read(4))
            relocs.append((skip, patch))
            total_patched += patch
        if total_patched != count:
            raise ValueError("Relocation patch count mismatch")
        return relocs

    def read(self, filepath):
        with open(filepath, 'rb') as f:
            data = f.read()
        with open(filepath, 'rb') as f:
            self.magic = f.read(4)
            if self.magic != b'3DSX':
                raise ValueError("Invalid magic")
            self.header_size, = struct.unpack('<H', f.read(2))
            self.reloc_hdr_size, = struct.unpack('<H', f.read(2))
            self.format_version, = struct.unpack('<I', f.read(4))
            self.flags, = struct.unpack('<I', f.read(4))
            self.code_size, = struct.unpack('<I', f.read(4))
            self.rodata_size, = struct.unpack('<I', f.read(4))
            self.data_size, = struct.unpack('<I', f.read(4))
            self.bss_size, = struct.unpack('<I', f.read(4))
            self.has_extended = self.header_size > 0x20
            if self.has_extended:
                self.smdh_offset, = struct.unpack('<I', f.read(4))
                self.smdh_size, = struct.unpack('<I', f.read(4))
                self.romfs_offset, = struct.unpack('<I', f.read(4))
            reloc_start = self.header_size
            f.seek(reloc_start)
            self.code_abs_count, self.code_rel_count = struct.unpack('<II', f.read(8))
            self.rodata_abs_count, self.rodata_rel_count = struct.unpack('<II', f.read(8))
            self.data_abs_count, self.data_rel_count = struct.unpack('<II', f.read(8))
            segments_start = self.header_size + 3 * self.reloc_hdr_size
            f.seek(segments_start)
            self.code_data = f.read(self.code_size)
            self.rodata_data = f.read(self.rodata_size)
            self.data_data = f.read(self.data_size - self.bss_size)
            relocs_start = segments_start + self.code_size + self.rodata_size + (self.data_size - self.bss_size)
            f.seek(relocs_start)
            self.code_abs_relocs = self.read_relocs(f, self.code_abs_count)
            self.code_rel_relocs = self.read_relocs(f, self.code_rel_count)
            self.rodata_abs_relocs = self.read_relocs(f, self.rodata_abs_count)
            self.rodata_rel_relocs = self.read_relocs(f, self.rodata_rel_count)
            self.data_abs_relocs = self.read_relocs(f, self.data_abs_count)
            self.data_rel_relocs = self.read_relocs(f, self.data_rel_count)
            if self.has_extended:
                if self.smdh_offset > 0:
                    f.seek(self.smdh_offset)
                    self.smdh_data = f.read(self.smdh_size)
                if self.romfs_offset > 0:
                    f.seek(self.romfs_offset)
                    self.romfs_data = f.read()

    def write_relocs(self, f, relocs):
        for skip, patch in relocs:
            f.write(struct.pack('<HH', skip, patch))

    def write(self, filepath):
        with open(filepath, 'wb') as f:
            f.write(self.magic)
            f.write(struct.pack('<H', self.header_size))
            f.write(struct.pack('<H', self.reloc_hdr_size))
            f.write(struct.pack('<I', self.format_version))
            f.write(struct.pack('<I', self.flags))
            f.write(struct.pack('<I', self.code_size))
            f.write(struct.pack('<I', self.rodata_size))
            f.write(struct.pack('<I', self.data_size))
            f.write(struct.pack('<I', self.bss_size))
            if self.has_extended:
                f.write(struct.pack('<I', self.smdh_offset))
                f.write(struct.pack('<I', self.smdh_size))
                f.write(struct.pack('<I', self.romfs_offset))
            f.write(struct.pack('<II', self.code_abs_count, self.code_rel_count))
            f.write(struct.pack('<II', self.rodata_abs_count, self.rodata_rel_count))
            f.write(struct.pack('<II', self.data_abs_count, self.data_rel_count))
            f.write(self.code_data)
            f.write(self.rodata_data)
            f.write(self.data_data)
            self.write_relocs(f, self.code_abs_relocs)
            self.write_relocs(f, self.code_rel_relocs)
            self.write_relocs(f, self.rodata_abs_relocs)
            self.write_relocs(f, self.rodata_rel_relocs)
            self.write_relocs(f, self.data_abs_relocs)
            self.write_relocs(f, self.data_rel_relocs)
            if self.has_extended:
                if self.smdh_size > 0:
                    current_pos = f.tell()
                    if current_pos != self.smdh_offset:
                        raise ValueError("SMDH offset mismatch")
                    f.write(self.smdh_data)
                if len(self.romfs_data) > 0:
                    current_pos = f.tell()
                    if current_pos != self.romfs_offset:
                        raise ValueError("RomFS offset mismatch")
                    f.write(self.romfs_data)

3. Java Class

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

public class ThreeDSXFile {
    private String magic = "3DSX";
    private short headerSize = 0x20;
    private short relocHdrSize = 0x08;
    private int formatVersion = 0;
    private int flags = 0;
    private int codeSize = 0;
    private int rodataSize = 0;
    private int dataSize = 0;
    private int bssSize = 0;
    private boolean hasExtended = false;
    private int smdhOffset = 0;
    private int smdhSize = 0;
    private int romfsOffset = 0;
    private int codeAbsCount = 0;
    private int codeRelCount = 0;
    private int rodataAbsCount = 0;
    private int rodataRelCount = 0;
    private int dataAbsCount = 0;
    private int dataRelCount = 0;
    private byte[] codeData = new byte[0];
    private byte[] rodataData = new byte[0];
    private byte[] dataData = new byte[0];
    private List<int[]> codeAbsRelocs = new ArrayList<>();
    private List<int[]> codeRelRelocs = new ArrayList<>();
    private List<int[]> rodataAbsRelocs = new ArrayList<>();
    private List<int[]> rodataRelRelocs = new ArrayList<>();
    private List<int[]> dataAbsRelocs = new ArrayList<>();
    private List<int[]> dataRelRelocs = new ArrayList<>();
    private byte[] smdhData = new byte[0];
    private byte[] romfsData = new byte[0];

    public ThreeDSXFile(String filepath) throws IOException {
        if (filepath != null) {
            read(filepath);
        }
    }

    private List<int[]> readRelocs(RandomAccessFile f, int count) throws IOException {
        List<int[]> relocs = new ArrayList<>();
        int totalPatched = 0;
        while (totalPatched < count) {
            short skip = Short.reverseBytes(f.readShort());
            short patch = Short.reverseBytes(f.readShort());
            relocs.add(new int[]{skip & 0xFFFF, patch & 0xFFFF});
            totalPatched += patch & 0xFFFF;
        }
        if (totalPatched != count) {
            throw new IOException("Relocation patch count mismatch");
        }
        return relocs;
    }

    public void read(String filepath) throws IOException {
        try (RandomAccessFile f = new RandomAccessFile(filepath, "r")) {
            ByteBuffer buf = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
            f.read(buf.array());
            magic = new String(buf.array());
            if (!magic.equals("3DSX")) {
                throw new IOException("Invalid magic");
            }
            headerSize = Short.reverseBytes(f.readShort());
            relocHdrSize = Short.reverseBytes(f.readShort());
            formatVersion = Integer.reverseBytes(f.readInt());
            flags = Integer.reverseBytes(f.readInt());
            codeSize = Integer.reverseBytes(f.readInt());
            rodataSize = Integer.reverseBytes(f.readInt());
            dataSize = Integer.reverseBytes(f.readInt());
            bssSize = Integer.reverseBytes(f.readInt());
            hasExtended = headerSize > 0x20;
            if (hasExtended) {
                smdhOffset = Integer.reverseBytes(f.readInt());
                smdhSize = Integer.reverseBytes(f.readInt());
                romfsOffset = Integer.reverseBytes(f.readInt());
            }
            long relocStart = headerSize;
            f.seek(relocStart);
            codeAbsCount = Integer.reverseBytes(f.readInt());
            codeRelCount = Integer.reverseBytes(f.readInt());
            rodataAbsCount = Integer.reverseBytes(f.readInt());
            rodataRelCount = Integer.reverseBytes(f.readInt());
            dataAbsCount = Integer.reverseBytes(f.readInt());
            dataRelCount = Integer.reverseBytes(f.readInt());
            long segmentsStart = headerSize + 3L * relocHdrSize;
            f.seek(segmentsStart);
            codeData = new byte[codeSize];
            f.read(codeData);
            rodataData = new byte[rodataSize];
            f.read(rodataData);
            dataData = new byte[dataSize - bssSize];
            f.read(dataData);
            long relocsStart = segmentsStart + codeSize + rodataSize + (dataSize - bssSize);
            f.seek(relocsStart);
            codeAbsRelocs = readRelocs(f, codeAbsCount);
            codeRelRelocs = readRelocs(f, codeRelCount);
            rodataAbsRelocs = readRelocs(f, rodataAbsCount);
            rodataRelRelocs = readRelocs(f, rodataRelCount);
            dataAbsRelocs = readRelocs(f, dataAbsCount);
            dataRelRelocs = readRelocs(f, dataRelCount);
            if (hasExtended) {
                if (smdhOffset > 0) {
                    f.seek(smdhOffset);
                    smdhData = new byte[smdhSize];
                    f.read(smdhData);
                }
                if (romfsOffset > 0) {
                    f.seek(romfsOffset);
                    FileChannel channel = f.getChannel();
                    long remaining = channel.size() - romfsOffset;
                    romfsData = new byte[(int) remaining];
                    f.read(romfsData);
                }
            }
        }
    }

    private void writeRelocs(RandomAccessFile f, List<int[]> relocs) throws IOException {
        for (int[] pair : relocs) {
            f.writeShort(Short.reverseBytes((short) pair[0]));
            f.writeShort(Short.reverseBytes((short) pair[1]));
        }
    }

    public void write(String filepath) throws IOException {
        try (RandomAccessFile f = new RandomAccessFile(filepath, "rw")) {
            f.write(magic.getBytes());
            f.writeShort(Short.reverseBytes(headerSize));
            f.writeShort(Short.reverseBytes(relocHdrSize));
            f.writeInt(Integer.reverseBytes(formatVersion));
            f.writeInt(Integer.reverseBytes(flags));
            f.writeInt(Integer.reverseBytes(codeSize));
            f.writeInt(Integer.reverseBytes(rodataSize));
            f.writeInt(Integer.reverseBytes(dataSize));
            f.writeInt(Integer.reverseBytes(bssSize));
            if (hasExtended) {
                f.writeInt(Integer.reverseBytes(smdhOffset));
                f.writeInt(Integer.reverseBytes(smdhSize));
                f.writeInt(Integer.reverseBytes(romfsOffset));
            }
            f.writeInt(Integer.reverseBytes(codeAbsCount));
            f.writeInt(Integer.reverseBytes(codeRelCount));
            f.writeInt(Integer.reverseBytes(rodataAbsCount));
            f.writeInt(Integer.reverseBytes(rodataRelCount));
            f.writeInt(Integer.reverseBytes(dataAbsCount));
            f.writeInt(Integer.reverseBytes(dataRelCount));
            f.write(codeData);
            f.write(rodataData);
            f.write(dataData);
            writeRelocs(f, codeAbsRelocs);
            writeRelocs(f, codeRelRelocs);
            writeRelocs(f, rodataAbsRelocs);
            writeRelocs(f, rodataRelRelocs);
            writeRelocs(f, dataAbsRelocs);
            writeRelocs(f, dataRelRelocs);
            if (hasExtended) {
                long currentPos = f.getFilePointer();
                if (smdhSize > 0 && currentPos != smdhOffset) {
                    throw new IOException("SMDH offset mismatch");
                }
                f.write(smdhData);
                currentPos = f.getFilePointer();
                if (romfsData.length > 0 && currentPos != romfsOffset) {
                    throw new IOException("RomFS offset mismatch");
                }
                f.write(romfsData);
            }
        }
    }
}

4. JavaScript Class

const fs = require('fs');

class ThreeDSXFile {
    constructor(filepath = null) {
        this.magic = '3DSX';
        this.headerSize = 0x20;
        this.relocHdrSize = 0x08;
        this.formatVersion = 0;
        this.flags = 0;
        this.codeSize = 0;
        this.rodataSize = 0;
        this.dataSize = 0;
        this.bssSize = 0;
        this.hasExtended = false;
        this.smdhOffset = 0;
        this.smdhSize = 0;
        this.romfsOffset = 0;
        this.codeAbsCount = 0;
        this.codeRelCount = 0;
        this.rodataAbsCount = 0;
        this.rodataRelCount = 0;
        this.dataAbsCount = 0;
        this.dataRelCount = 0;
        this.codeData = Buffer.alloc(0);
        this.rodataData = Buffer.alloc(0);
        this.dataData = Buffer.alloc(0);
        this.codeAbsRelocs = [];
        this.codeRelRelocs = [];
        this.rodataAbsRelocs = [];
        this.rodataRelRelocs = [];
        this.dataAbsRelocs = [];
        this.dataRelRelocs = [];
        this.smdhData = Buffer.alloc(0);
        this.romfsData = Buffer.alloc(0);
        if (filepath) {
            this.read(filepath);
        }
    }

    readRelocs(buffer, offset, count) {
        const relocs = [];
        let totalPatched = 0;
        let pos = offset;
        while (totalPatched < count) {
            const skip = buffer.readUInt16LE(pos);
            const patch = buffer.readUInt16LE(pos + 2);
            relocs.push([skip, patch]);
            totalPatched += patch;
            pos += 4;
        }
        if (totalPatched !== count) {
            throw new Error('Relocation patch count mismatch');
        }
        return { relocs, newOffset: pos };
    }

    read(filepath) {
        const data = fs.readFileSync(filepath);
        let pos = 0;
        this.magic = data.slice(pos, pos + 4).toString();
        pos += 4;
        if (this.magic !== '3DSX') {
            throw new Error('Invalid magic');
        }
        this.headerSize = data.readUInt16LE(pos);
        pos += 2;
        this.relocHdrSize = data.readUInt16LE(pos);
        pos += 2;
        this.formatVersion = data.readUInt32LE(pos);
        pos += 4;
        this.flags = data.readUInt32LE(pos);
        pos += 4;
        this.codeSize = data.readUInt32LE(pos);
        pos += 4;
        this.rodataSize = data.readUInt32LE(pos);
        pos += 4;
        this.dataSize = data.readUInt32LE(pos);
        pos += 4;
        this.bssSize = data.readUInt32LE(pos);
        pos += 4;
        this.hasExtended = this.headerSize > 0x20;
        if (this.hasExtended) {
            this.smdhOffset = data.readUInt32LE(pos);
            pos += 4;
            this.smdhSize = data.readUInt32LE(pos);
            pos += 4;
            this.romfsOffset = data.readUInt32LE(pos);
            pos += 4;
        }
        this.codeAbsCount = data.readUInt32LE(pos);
        pos += 4;
        this.codeRelCount = data.readUInt32LE(pos);
        pos += 4;
        this.rodataAbsCount = data.readUInt32LE(pos);
        pos += 4;
        this.rodataRelCount = data.readUInt32LE(pos);
        pos += 4;
        this.dataAbsCount = data.readUInt32LE(pos);
        pos += 4;
        this.dataRelCount = data.readUInt32LE(pos);
        pos += 4;
        this.codeData = data.slice(pos, pos + this.codeSize);
        pos += this.codeSize;
        this.rodataData = data.slice(pos, pos + this.rodataSize);
        pos += this.rodataSize;
        this.dataData = data.slice(pos, pos + this.dataSize - this.bssSize);
        pos += this.dataSize - this.bssSize;
        let result = this.readRelocs(data, pos, this.codeAbsCount);
        this.codeAbsRelocs = result.relocs;
        pos = result.newOffset;
        result = this.readRelocs(data, pos, this.codeRelCount);
        this.codeRelRelocs = result.relocs;
        pos = result.newOffset;
        result = this.readRelocs(data, pos, this.rodataAbsCount);
        this.rodataAbsRelocs = result.relocs;
        pos = result.newOffset;
        result = this.readRelocs(data, pos, this.rodataRelCount);
        this.rodataRelRelocs = result.relocs;
        pos = result.newOffset;
        result = this.readRelocs(data, pos, this.dataAbsCount);
        this.dataAbsRelocs = result.relocs;
        pos = result.newOffset;
        result = this.readRelocs(data, pos, this.dataRelCount);
        this.dataRelRelocs = result.relocs;
        pos = result.newOffset;
        if (this.hasExtended) {
            if (this.smdhOffset > 0) {
                this.smdhData = data.slice(this.smdhOffset, this.smdhOffset + this.smdhSize);
            }
            if (this.romfsOffset > 0) {
                this.romfsData = data.slice(this.romfsOffset);
            }
        }
    }

    writeRelocs(buffer, offset, relocs) {
        let pos = offset;
        for (const [skip, patch] of relocs) {
            buffer.writeUInt16LE(skip, pos);
            buffer.writeUInt16LE(patch, pos + 2);
            pos += 4;
        }
        return pos;
    }

    write(filepath) {
        let totalSize = this.headerSize + 3 * this.relocHdrSize + this.codeSize + this.rodataSize + (this.dataSize - this.bssSize);
        totalSize += (this.codeAbsRelocs.length + this.codeRelRelocs.length + this.rodataAbsRelocs.length + this.rodataRelRelocs.length + this.dataAbsRelocs.length + this.dataRelRelocs.length) * 4;
        if (this.hasExtended) {
            if (this.smdhSize > 0) {
                this.smdhOffset = totalSize;
                totalSize += this.smdhSize;
            }
            if (this.romfsData.length > 0) {
                this.romfsOffset = totalSize;
                totalSize += this.romfsData.length;
            }
        }
        const buffer = Buffer.alloc(totalSize);
        let pos = 0;
        buffer.write(this.magic, pos);
        pos += 4;
        buffer.writeUInt16LE(this.headerSize, pos);
        pos += 2;
        buffer.writeUInt16LE(this.relocHdrSize, pos);
        pos += 2;
        buffer.writeUInt32LE(this.formatVersion, pos);
        pos += 4;
        buffer.writeUInt32LE(this.flags, pos);
        pos += 4;
        buffer.writeUInt32LE(this.codeSize, pos);
        pos += 4;
        buffer.writeUInt32LE(this.rodataSize, pos);
        pos += 4;
        buffer.writeUInt32LE(this.dataSize, pos);
        pos += 4;
        buffer.writeUInt32LE(this.bssSize, pos);
        pos += 4;
        if (this.hasExtended) {
            buffer.writeUInt32LE(this.smdhOffset, pos);
            pos += 4;
            buffer.writeUInt32LE(this.smdhSize, pos);
            pos += 4;
            buffer.writeUInt32LE(this.romfsOffset, pos);
            pos += 4;
        }
        buffer.writeUInt32LE(this.codeAbsCount, pos);
        pos += 4;
        buffer.writeUInt32LE(this.codeRelCount, pos);
        pos += 4;
        buffer.writeUInt32LE(this.rodataAbsCount, pos);
        pos += 4;
        buffer.writeUInt32LE(this.rodataRelCount, pos);
        pos += 4;
        buffer.writeUInt32LE(this.dataAbsCount, pos);
        pos += 4;
        buffer.writeUInt32LE(this.dataRelCount, pos);
        pos += 4;
        this.codeData.copy(buffer, pos);
        pos += this.codeSize;
        this.rodataData.copy(buffer, pos);
        pos += this.rodataSize;
        this.dataData.copy(buffer, pos);
        pos += this.dataSize - this.bssSize;
        pos = this.writeRelocs(buffer, pos, this.codeAbsRelocs);
        pos = this.writeRelocs(buffer, pos, this.codeRelRelocs);
        pos = this.writeRelocs(buffer, pos, this.rodataAbsRelocs);
        pos = this.writeRelocs(buffer, pos, this.rodataRelRelocs);
        pos = this.writeRelocs(buffer, pos, this.dataAbsRelocs);
        pos = this.writeRelocs(buffer, pos, this.dataRelRelocs);
        if (this.hasExtended) {
            if (this.smdhSize > 0) {
                this.smdhData.copy(buffer, pos);
                pos += this.smdhSize;
            }
            if (this.romfsData.length > 0) {
                this.romfsData.copy(buffer, pos);
            }
        }
        fs.writeFileSync(filepath, buffer);
    }
}

5. C Class (Struct with Functions)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef struct {
    uint16_t skip;
    uint16_t patch;
} RelocPair;

typedef struct {
    char magic[4];
    uint16_t header_size;
    uint16_t reloc_hdr_size;
    uint32_t format_version;
    uint32_t flags;
    uint32_t code_size;
    uint32_t rodata_size;
    uint32_t data_size;
    uint32_t bss_size;
    int has_extended;
    uint32_t smdh_offset;
    uint32_t smdh_size;
    uint32_t romfs_offset;
    uint32_t code_abs_count;
    uint32_t code_rel_count;
    uint32_t rodata_abs_count;
    uint32_t rodata_rel_count;
    uint32_t data_abs_count;
    uint32_t data_rel_count;
    uint8_t *code_data;
    uint8_t *rodata_data;
    uint8_t *data_data;
    RelocPair *code_abs_relocs;
    size_t code_abs_relocs_len;
    RelocPair *code_rel_relocs;
    size_t code_rel_relocs_len;
    RelocPair *rodata_abs_relocs;
    size_t rodata_abs_relocs_len;
    RelocPair *rodata_rel_relocs;
    size_t rodata_rel_relocs_len;
    RelocPair *data_abs_relocs;
    size_t data_abs_relocs_len;
    RelocPair *data_rel_relocs;
    size_t data_rel_relocs_len;
    uint8_t *smdh_data;
    uint8_t *romfs_data;
    size_t romfs_size;
} ThreeDSXFile;

ThreeDSXFile *threedsx_new() {
    ThreeDSXFile *self = calloc(1, sizeof(ThreeDSXFile));
    memcpy(self->magic, "3DSX", 4);
    self->header_size = 0x20;
    self->reloc_hdr_size = 0x08;
    return self;
}

void threedsx_free(ThreeDSXFile *self) {
    free(self->code_data);
    free(self->rodata_data);
    free(self->data_data);
    free(self->code_abs_relocs);
    free(self->code_rel_relocs);
    free(self->rodata_abs_relocs);
    free(self->rodata_rel_relocs);
    free(self->data_abs_relocs);
    free(self->data_rel_relocs);
    free(self->smdh_data);
    free(self->romfs_data);
    free(self);
}

RelocPair *read_relocs(FILE *f, uint32_t count, size_t *out_len) {
    RelocPair *relocs = NULL;
    size_t capacity = 0;
    size_t len = 0;
    uint32_t total_patched = 0;
    while (total_patched < count) {
        if (len >= capacity) {
            capacity = capacity ? capacity * 2 : 8;
            relocs = realloc(relocs, capacity * sizeof(RelocPair));
        }
        fread(&relocs[len].skip, 2, 1, f);
        fread(&relocs[len].patch, 2, 1, f);
        total_patched += relocs[len].patch;
        len++;
    }
    if (total_patched != count) {
        free(relocs);
        return NULL;
    }
    *out_len = len;
    return relocs;
}

int threedsx_read(ThreeDSXFile *self, const char *filepath) {
    FILE *f = fopen(filepath, "rb");
    if (!f) return -1;
    fseek(f, 0, SEEK_END);
    long file_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    fread(self->magic, 4, 1, f);
    if (strncmp(self->magic, "3DSX", 4) != 0) {
        fclose(f);
        return -1;
    }
    fread(&self->header_size, 2, 1, f);
    fread(&self->reloc_hdr_size, 2, 1, f);
    fread(&self->format_version, 4, 1, f);
    fread(&self->flags, 4, 1, f);
    fread(&self->code_size, 4, 1, f);
    fread(&self->rodata_size, 4, 1, f);
    fread(&self->data_size, 4, 1, f);
    fread(&self->bss_size, 4, 1, f);
    self->has_extended = self->header_size > 0x20;
    if (self->has_extended) {
        fread(&self->smdh_offset, 4, 1, f);
        fread(&self->smdh_size, 4, 1, f);
        fread(&self->romfs_offset, 4, 1, f);
    }
    fseek(f, self->header_size, SEEK_SET);
    fread(&self->code_abs_count, 4, 1, f);
    fread(&self->code_rel_count, 4, 1, f);
    fread(&self->rodata_abs_count, 4, 1, f);
    fread(&self->rodata_rel_count, 4, 1, f);
    fread(&self->data_abs_count, 4, 1, f);
    fread(&self->data_rel_count, 4, 1, f);
    long segments_start = self->header_size + 3 * self->reloc_hdr_size;
    fseek(f, segments_start, SEEK_SET);
    self->code_data = malloc(self->code_size);
    fread(self->code_data, self->code_size, 1, f);
    self->rodata_data = malloc(self->rodata_size);
    fread(self->rodata_data, self->rodata_size, 1, f);
    self->data_data = malloc(self->data_size - self->bss_size);
    fread(self->data_data, self->data_size - self->bss_size, 1, f);
    long relocs_start = segments_start + self->code_size + self->rodata_size + (self->data_size - self->bss_size);
    fseek(f, relocs_start, SEEK_SET);
    self->code_abs_relocs = read_relocs(f, self->code_abs_count, &self->code_abs_relocs_len);
    self->code_rel_relocs = read_relocs(f, self->code_rel_count, &self->code_rel_relocs_len);
    self->rodata_abs_relocs = read_relocs(f, self->rodata_abs_count, &self->rodata_abs_relocs_len);
    self->rodata_rel_relocs = read_relocs(f, self->rodata_rel_count, &self->rodata_rel_relocs_len);
    self->data_abs_relocs = read_relocs(f, self->data_abs_count, &self->data_abs_relocs_len);
    self->data_rel_relocs = read_relocs(f, self->data_rel_count, &self->data_rel_relocs_len);
    if (self->has_extended) {
        if (self->smdh_offset > 0) {
            fseek(f, self->smdh_offset, SEEK_SET);
            self->smdh_data = malloc(self->smdh_size);
            fread(self->smdh_data, self->smdh_size, 1, f);
        }
        if (self->romfs_offset > 0) {
            fseek(f, self->romfs_offset, SEEK_SET);
            self->romfs_size = file_size - self->romfs_offset;
            self->romfs_data = malloc(self->romfs_size);
            fread(self->romfs_data, self->romfs_size, 1, f);
        }
    }
    fclose(f);
    return 0;
}

void write_relocs(FILE *f, RelocPair *relocs, size_t len) {
    for (size_t i = 0; i < len; i++) {
        fwrite(&relocs[i].skip, 2, 1, f);
        fwrite(&relocs[i].patch, 2, 1, f);
    }
}

int threedsx_write(ThreeDSXFile *self, const char *filepath) {
    FILE *f = fopen(filepath, "wb");
    if (!f) return -1;
    fwrite(self->magic, 4, 1, f);
    fwrite(&self->header_size, 2, 1, f);
    fwrite(&self->reloc_hdr_size, 2, 1, f);
    fwrite(&self->format_version, 4, 1, f);
    fwrite(&self->flags, 4, 1, f);
    fwrite(&self->code_size, 4, 1, f);
    fwrite(&self->rodata_size, 4, 1, f);
    fwrite(&self->data_size, 4, 1, f);
    fwrite(&self->bss_size, 4, 1, f);
    if (self->has_extended) {
        fwrite(&self->smdh_offset, 4, 1, f);
        fwrite(&self->smdh_size, 4, 1, f);
        fwrite(&self->romfs_offset, 4, 1, f);
    }
    fwrite(&self->code_abs_count, 4, 1, f);
    fwrite(&self->code_rel_count, 4, 1, f);
    fwrite(&self->rodata_abs_count, 4, 1, f);
    fwrite(&self->rodata_rel_count, 4, 1, f);
    fwrite(&self->data_abs_count, 4, 1, f);
    fwrite(&self->data_rel_count, 4, 1, f);
    fwrite(self->code_data, self->code_size, 1, f);
    fwrite(self->rodata_data, self->rodata_size, 1, f);
    fwrite(self->data_data, self->data_size - self->bss_size, 1, f);
    write_relocs(f, self->code_abs_relocs, self->code_abs_relocs_len);
    write_relocs(f, self->code_rel_relocs, self->code_rel_relocs_len);
    write_relocs(f, self->rodata_abs_relocs, self->rodata_abs_relocs_len);
    write_relocs(f, self->rodata_rel_relocs, self->rodata_rel_relocs_len);
    write_relocs(f, self->data_abs_relocs, self->data_abs_relocs_len);
    write_relocs(f, self->data_rel_relocs, self->data_rel_relocs_len);
    if (self->has_extended) {
        long current_pos = ftell(f);
        if (self->smdh_size > 0 && current_pos != self->smdh_offset) {
            fclose(f);
            return -1;
        }
        fwrite(self->smdh_data, self->smdh_size, 1, f);
        current_pos = ftell(f);
        if (self->romfs_size > 0 && current_pos != self->romfs_offset) {
            fclose(f);
            return -1;
        }
        fwrite(self->romfs_data, self->romfs_size, 1, f);
    }
    fclose(f);
    return 0;
}