Task 002: .3DSX File Format

Task 002: .3DSX File Format

1. Properties of the .3DSX File Format

The .3DSX file format is an executable format designed for homebrew applications on the Nintendo 3DS console. It includes a structured header, optional extended header, relocation tables, code segments, and optional metadata sections. The intrinsic properties, derived from the file's binary structure and specifications, are as follows:

  • Magic Identifier: A 4-byte string "3DSX" at offset 0x0, serving as the file signature.
  • Header Size: A 2-byte unsigned integer at offset 0x4, indicating the total size of the header (typically 0x20 for basic headers or 0x2C for extended headers).
  • Relocation Header Size: A 2-byte unsigned integer at offset 0x6, specifying the size of each relocation table header (typically 0x08).
  • Format Version: A 4-byte unsigned integer at offset 0x8, denoting the version of the format (commonly 0).
  • Flags: A 4-byte unsigned integer at offset 0xC, reserved for flags (often 0 in practice).
  • Code Segment Size: A 4-byte unsigned integer at offset 0x10, representing the size of the code segment in bytes.
  • Read-Only Data (Rodata) Segment Size: A 4-byte unsigned integer at offset 0x14, representing the size of the read-only data segment in bytes.
  • Data Segment Size: A 4-byte unsigned integer at offset 0x18, representing the size of the data segment in bytes (including the BSS section).
  • BSS Segment Size: A 4-byte unsigned integer at offset 0x1C, representing the size of the uninitialized data (BSS) section in bytes.
  • SMDH Offset (extended header only): A 4-byte unsigned integer at offset 0x20 (relative to header start), indicating the file offset to the SMDH (icon and title metadata) section, if present.
  • SMDH Size (extended header only): A 4-byte unsigned integer at offset 0x24, specifying the size of the SMDH section in bytes (typically 0x36C0 if present).
  • RomFS Offset (extended header only): A 4-byte unsigned integer at offset 0x28, indicating the file offset to the RomFS (embedded file system) section, if present.
  • Code Relocation Counts: In the code relocation header (8 bytes following the main header), 4-byte unsigned integers for the number of absolute and relative relocations (relative often 0).
  • Rodata Relocation Counts: Similar to code, in the rodata relocation header (next 8 bytes), counts for absolute and relative relocations.
  • Data Relocation Counts: Similar to above, in the data relocation header (next 8 bytes), counts for absolute and relative relocations.

These properties define the file's layout, including segment alignments (typically 0x1000-byte aligned) and relocation entries (each 4 bytes: 2-byte skip count and 2-byte patch count, repeated per relocation count).

The following are two direct download links to sample .3DSX files from public repositories:

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .3DSX File Dumping

The following is a self-contained HTML snippet with embedded JavaScript that can be inserted into a Ghost blog post (or any HTML-based platform). It enables users to drag and drop a .3DSX file, parses the file using a DataView, extracts the properties listed above, and displays them on the screen.

Drag and drop a .3DSX file here.

4. Python Class for .3DSX File Handling

The following Python class uses the struct module to decode, read, write, and print the properties of a .3DSX file. It assumes little-endian byte order based on the format specification. The read method parses the file and stores properties in a dictionary. The print_properties method outputs them to the console. The write method allows creating or modifying a file with given properties (minimal implementation, focusing on header; full segment writing requires additional data).

import struct

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

    def read(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        
        # Unpack header
        magic, header_size, reloc_hdr_size, format_ver, flags, code_size, rodata_size, data_size, bss_size = struct.unpack('<4sHHIIIII', data[:32])
        self.properties['magic'] = magic.decode('ascii')
        self.properties['header_size'] = header_size
        self.properties['reloc_header_size'] = reloc_hdr_size
        self.properties['format_version'] = format_ver
        self.properties['flags'] = flags
        self.properties['code_segment_size'] = code_size
        self.properties['rodata_segment_size'] = rodata_size
        self.properties['data_segment_size'] = data_size
        self.properties['bss_segment_size'] = bss_size

        offset = 32
        if header_size > 32:
            smdh_offset, smdh_size, romfs_offset = struct.unpack('<III', data[offset:offset+12])
            self.properties['smdh_offset'] = smdh_offset
            self.properties['smdh_size'] = smdh_size
            self.properties['romfs_offset'] = romfs_offset
            offset += 12

        # Relocation headers
        reloc_start = offset
        code_abs, code_rel = struct.unpack('<II', data[reloc_start:reloc_start+8])
        rodata_abs, rodata_rel = struct.unpack('<II', data[reloc_start+8:reloc_start+16])
        data_abs, data_rel = struct.unpack('<II', data[reloc_start+16:reloc_start+24])
        self.properties['code_abs_relocs'] = code_abs
        self.properties['code_rel_relocs'] = code_rel
        self.properties['rodata_abs_relocs'] = rodata_abs
        self.properties['rodata_rel_relocs'] = rodata_rel
        self.properties['data_abs_relocs'] = data_abs
        self.properties['data_rel_relocs'] = data_rel

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key.capitalize().replace('_', ' ')}: {value if isinstance(value, str) else hex(value)}")

    def write(self, output_path, properties=None):
        if properties is None:
            properties = self.properties
        
        header = struct.pack('<4sHHIIIII', 
                             properties['magic'].encode('ascii'), 
                             properties['header_size'], 
                             properties['reloc_header_size'], 
                             properties['format_version'], 
                             properties['flags'], 
                             properties['code_segment_size'], 
                             properties['rodata_segment_size'], 
                             properties['data_segment_size'], 
                             properties['bss_segment_size'])
        
        extended = b''
        if properties['header_size'] > 32:
            extended = struct.pack('<III', 
                                   properties['smdh_offset'], 
                                   properties['smdh_size'], 
                                   properties['romfs_offset'])
        
        relocs = struct.pack('<IIIIII', 
                             properties['code_abs_relocs'], properties['code_rel_relocs'],
                             properties['rodata_abs_relocs'], properties['rodata_rel_relocs'],
                             properties['data_abs_relocs'], properties['data_rel_relocs'])
        
        with open(output_path, 'wb') as f:
            f.write(header + extended + relocs)
            # Additional segments/reloc entries would be appended here if provided

# Example usage:
# handler = ThreeDSXHandler('example.3dsx')
# handler.read()
# handler.print_properties()
# handler.write('output.3dsx')

5. Java Class for .3DSX File Handling

The following Java class uses ByteBuffer for decoding, reading, writing, and printing the properties. It handles little-endian order. The read method parses the file into a Map. The printProperties method outputs to the console. The write method supports creating or modifying the header section.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;

public class ThreeDSXHandler {
    private String filepath;
    private Map<String, Object> properties = new HashMap<>();

    public ThreeDSXHandler(String filepath) {
        this.filepath = filepath;
    }

    public void read() throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath);
             FileChannel channel = fis.getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            channel.read(buffer);
            buffer.flip();

            byte[] magicBytes = new byte[4];
            buffer.get(magicBytes);
            properties.put("magic", new String(magicBytes));
            properties.put("header_size", (int) buffer.getShort());
            properties.put("reloc_header_size", (int) buffer.getShort());
            properties.put("format_version", buffer.getInt());
            properties.put("flags", buffer.getInt());
            properties.put("code_segment_size", buffer.getInt());
            properties.put("rodata_segment_size", buffer.getInt());
            properties.put("data_segment_size", buffer.getInt());
            properties.put("bss_segment_size", buffer.getInt());

            int headerSize = (int) properties.get("header_size");
            int offset = 32;
            if (headerSize > 32) {
                properties.put("smdh_offset", buffer.getInt(offset));
                properties.put("smdh_size", buffer.getInt(offset + 4));
                properties.put("romfs_offset", buffer.getInt(offset + 8));
                offset += 12;
            }

            int relocStart = offset;
            properties.put("code_abs_relocs", buffer.getInt(relocStart));
            properties.put("code_rel_relocs", buffer.getInt(relocStart + 4));
            properties.put("rodata_abs_relocs", buffer.getInt(relocStart + 8));
            properties.put("rodata_rel_relocs", buffer.getInt(relocStart + 12));
            properties.put("data_abs_relocs", buffer.getInt(relocStart + 16));
            properties.put("data_rel_relocs", buffer.getInt(relocStart + 20));
        }
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            String key = entry.getKey().replace("_", " ").toUpperCase();
            Object value = entry.getValue();
            String valStr = (value instanceof String) ? (String) value : "0x" + Integer.toHexString((Integer) value);
            System.out.println(key + ": " + valStr);
        }
    }

    public void write(String outputPath, Map<String, Object> props) throws IOException {
        if (props == null) {
            props = properties;
        }
        ByteBuffer buffer = ByteBuffer.allocate(64); // Sufficient for header + relocs
        buffer.order(ByteOrder.LITTLE_ENDIAN);

        buffer.put(((String) props.get("magic")).getBytes());
        buffer.putShort(((Integer) props.get("header_size")).shortValue());
        buffer.putShort(((Integer) props.get("reloc_header_size")).shortValue());
        buffer.putInt((Integer) props.get("format_version"));
        buffer.putInt((Integer) props.get("flags"));
        buffer.putInt((Integer) props.get("code_segment_size"));
        buffer.putInt((Integer) props.get("rodata_segment_size"));
        buffer.putInt((Integer) props.get("data_segment_size"));
        buffer.putInt((Integer) props.get("bss_segment_size"));

        int headerSize = (Integer) props.get("header_size");
        if (headerSize > 32) {
            buffer.putInt((Integer) props.get("smdh_offset"));
            buffer.putInt((Integer) props.get("smdh_size"));
            buffer.putInt((Integer) props.get("romfs_offset"));
        }

        buffer.putInt((Integer) props.get("code_abs_relocs"));
        buffer.putInt((Integer) props.get("code_rel_relocs"));
        buffer.putInt((Integer) props.get("rodata_abs_relocs"));
        buffer.putInt((Integer) props.get("rodata_rel_relocs"));
        buffer.putInt((Integer) props.get("data_abs_relocs"));
        buffer.putInt((Integer) props.get("data_rel_relocs"));

        buffer.flip();
        try (FileOutputStream fos = new FileOutputStream(outputPath);
             FileChannel channel = fos.getChannel()) {
            channel.write(buffer);
            // Append segments/reloc entries if needed
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     ThreeDSXHandler handler = new ThreeDSXHandler("example.3dsx");
    //     handler.read();
    //     handler.printProperties();
    //     handler.write("output.3dsx", null);
    // }
}

6. JavaScript Class for .3DSX File Handling

The following JavaScript class uses DataView for browser-based or Node.js file handling (with fs for Node). The read method parses an ArrayBuffer and stores properties. The printProperties method logs to the console. The write method generates a new ArrayBuffer for the header.

class ThreeDSXHandler {
  constructor(buffer) {
    this.buffer = buffer; // ArrayBuffer
    this.properties = {};
  }

  read() {
    const dataView = new DataView(this.buffer);

    const magic = String.fromCharCode(...new Uint8Array(this.buffer.slice(0, 4)));
    this.properties.magic = magic;
    this.properties.header_size = dataView.getUint16(4, true);
    this.properties.reloc_header_size = dataView.getUint16(6, true);
    this.properties.format_version = dataView.getUint32(8, true);
    this.properties.flags = dataView.getUint32(12, true);
    this.properties.code_segment_size = dataView.getUint32(16, true);
    this.properties.rodata_segment_size = dataView.getUint32(20, true);
    this.properties.data_segment_size = dataView.getUint32(24, true);
    this.properties.bss_segment_size = dataView.getUint32(28, true);

    let offset = 32;
    if (this.properties.header_size > 32) {
      this.properties.smdh_offset = dataView.getUint32(offset, true);
      this.properties.smdh_size = dataView.getUint32(offset + 4, true);
      this.properties.romfs_offset = dataView.getUint32(offset + 8, true);
      offset += 12;
    }

    const relocStart = offset;
    this.properties.code_abs_relocs = dataView.getUint32(relocStart, true);
    this.properties.code_rel_relocs = dataView.getUint32(relocStart + 4, true);
    this.properties.rodata_abs_relocs = dataView.getUint32(relocStart + 8, true);
    this.properties.rodata_rel_relocs = dataView.getUint32(relocStart + 12, true);
    this.properties.data_abs_relocs = dataView.getUint32(relocStart + 16, true);
    this.properties.data_rel_relocs = dataView.getUint32(relocStart + 20, true);
  }

  printProperties() {
    for (const [key, value] of Object.entries(this.properties)) {
      console.log(`${key.replace(/_/g, ' ').toUpperCase()}: ${typeof value === 'string' ? value : '0x' + value.toString(16)}`);
    }
  }

  write() {
    const headerSize = this.properties.header_size;
    const bufferSize = headerSize + 24; // Header + relocs
    const newBuffer = new ArrayBuffer(bufferSize);
    const dataView = new DataView(newBuffer);

    const magicBytes = this.properties.magic.split('').map(c => c.charCodeAt(0));
    magicBytes.forEach((byte, i) => dataView.setUint8(i, byte));
    dataView.setUint16(4, this.properties.header_size, true);
    dataView.setUint16(6, this.properties.reloc_header_size, true);
    dataView.setUint32(8, this.properties.format_version, true);
    dataView.setUint32(12, this.properties.flags, true);
    dataView.setUint32(16, this.properties.code_segment_size, true);
    dataView.setUint32(20, this.properties.rodata_segment_size, true);
    dataView.setUint32(24, this.properties.data_segment_size, true);
    dataView.setUint32(28, this.properties.bss_segment_size, true);

    let offset = 32;
    if (headerSize > 32) {
      dataView.setUint32(offset, this.properties.smdh_offset, true);
      dataView.setUint32(offset + 4, this.properties.smdh_size, true);
      dataView.setUint32(offset + 8, this.properties.romfs_offset, true);
      offset += 12;
    }

    dataView.setUint32(offset, this.properties.code_abs_relocs, true);
    dataView.setUint32(offset + 4, this.properties.code_rel_relocs, true);
    dataView.setUint32(offset + 8, this.properties.rodata_abs_relocs, true);
    dataView.setUint32(offset + 12, this.properties.rodata_rel_relocs, true);
    dataView.setUint32(offset + 16, this.properties.data_abs_relocs, true);
    dataView.setUint32(offset + 20, this.properties.data_rel_relocs, true);

    return newBuffer; // Return ArrayBuffer for saving (e.g., via Blob)
  }
}

// Example Node.js usage:
// const fs = require('fs');
// const buffer = fs.readFileSync('example.3dsx').buffer;
// const handler = new ThreeDSXHandler(buffer);
// handler.read();
// handler.printProperties();
// const newBuffer = handler.write();
// fs.writeFileSync('output.3dsx', new Uint8Array(newBuffer));

7. C Implementation for .3DSX File Handling

Since C does not support classes natively, the following implementation uses a struct for properties and functions for decoding, reading, writing, and printing. It employs fread and fwrite for file operations, assuming little-endian. The read_3dsx function parses the file. The print_properties function outputs to stdout. The write_3dsx function writes the header to a new file.

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

typedef struct {
    char magic[5]; // Null-terminated
    uint16_t header_size;
    uint16_t reloc_header_size;
    uint32_t format_version;
    uint32_t flags;
    uint32_t code_segment_size;
    uint32_t rodata_segment_size;
    uint32_t data_segment_size;
    uint32_t bss_segment_size;
    uint32_t smdh_offset;
    uint32_t smdh_size;
    uint32_t romfs_offset;
    uint32_t code_abs_relocs;
    uint32_t code_rel_relocs;
    uint32_t rodata_abs_relocs;
    uint32_t rodata_rel_relocs;
    uint32_t data_abs_relocs;
    uint32_t data_rel_relocs;
    int has_extended; // Flag for extended header
} ThreeDSXProperties;

void read_3dsx(const char* filepath, ThreeDSXProperties* props) {
    FILE* f = fopen(filepath, "rb");
    if (!f) {
        perror("Failed to open file");
        return;
    }

    // Read header
    char magic[4];
    fread(magic, 1, 4, f);
    strncpy(props->magic, magic, 4);
    props->magic[4] = '\0';
    fread(&props->header_size, sizeof(uint16_t), 1, f);
    fread(&props->reloc_header_size, sizeof(uint16_t), 1, f);
    fread(&props->format_version, sizeof(uint32_t), 1, f);
    fread(&props->flags, sizeof(uint32_t), 1, f);
    fread(&props->code_segment_size, sizeof(uint32_t), 1, f);
    fread(&props->rodata_segment_size, sizeof(uint32_t), 1, f);
    fread(&props->data_segment_size, sizeof(uint32_t), 1, f);
    fread(&props->bss_segment_size, sizeof(uint32_t), 1, f);

    props->has_extended = (props->header_size > 32);
    if (props->has_extended) {
        fread(&props->smdh_offset, sizeof(uint32_t), 1, f);
        fread(&props->smdh_size, sizeof(uint32_t), 1, f);
        fread(&props->romfs_offset, sizeof(uint32_t), 1, f);
    }

    // Read relocation headers
    fread(&props->code_abs_relocs, sizeof(uint32_t), 1, f);
    fread(&props->code_rel_relocs, sizeof(uint32_t), 1, f);
    fread(&props->rodata_abs_relocs, sizeof(uint32_t), 1, f);
    fread(&props->rodata_rel_relocs, sizeof(uint32_t), 1, f);
    fread(&props->data_abs_relocs, sizeof(uint32_t), 1, f);
    fread(&props->data_rel_relocs, sizeof(uint32_t), 1, f);

    fclose(f);
}

void print_properties(const ThreeDSXProperties* props) {
    printf("Magic Identifier: %s\n", props->magic);
    printf("Header Size: 0x%X\n", props->header_size);
    printf("Relocation Header Size: 0x%X\n", props->reloc_header_size);
    printf("Format Version: %u\n", props->format_version);
    printf("Flags: 0x%X\n", props->flags);
    printf("Code Segment Size: 0x%X\n", props->code_segment_size);
    printf("Read-Only Data Segment Size: 0x%X\n", props->rodata_segment_size);
    printf("Data Segment Size: 0x%X\n", props->data_segment_size);
    printf("BSS Segment Size: 0x%X\n", props->bss_segment_size);
    if (props->has_extended) {
        printf("SMDH Offset: 0x%X\n", props->smdh_offset);
        printf("SMDH Size: 0x%X\n", props->smdh_size);
        printf("RomFS Offset: 0x%X\n", props->romfs_offset);
    }
    printf("Code Absolute Relocations: %u\n", props->code_abs_relocs);
    printf("Code Relative Relocations: %u\n", props->code_rel_relocs);
    printf("Rodata Absolute Relocations: %u\n", props->rodata_abs_relocs);
    printf("Rodata Relative Relocations: %u\n", props->rodata_rel_relocs);
    printf("Data Absolute Relocations: %u\n", props->data_abs_relocs);
    printf("Data Relative Relocations: %u\n", props->data_rel_relocs);
}

void write_3dsx(const char* output_path, const ThreeDSXProperties* props) {
    FILE* f = fopen(output_path, "wb");
    if (!f) {
        perror("Failed to open output file");
        return;
    }

    fwrite(props->magic, 1, 4, f);
    fwrite(&props->header_size, sizeof(uint16_t), 1, f);
    fwrite(&props->reloc_header_size, sizeof(uint16_t), 1, f);
    fwrite(&props->format_version, sizeof(uint32_t), 1, f);
    fwrite(&props->flags, sizeof(uint32_t), 1, f);
    fwrite(&props->code_segment_size, sizeof(uint32_t), 1, f);
    fwrite(&props->rodata_segment_size, sizeof(uint32_t), 1, f);
    fwrite(&props->data_segment_size, sizeof(uint32_t), 1, f);
    fwrite(&props->bss_segment_size, sizeof(uint32_t), 1, f);

    if (props->has_extended) {
        fwrite(&props->smdh_offset, sizeof(uint32_t), 1, f);
        fwrite(&props->smdh_size, sizeof(uint32_t), 1, f);
        fwrite(&props->romfs_offset, sizeof(uint32_t), 1, f);
    }

    fwrite(&props->code_abs_relocs, sizeof(uint32_t), 1, f);
    fwrite(&props->code_rel_relocs, sizeof(uint32_t), 1, f);
    fwrite(&props->rodata_abs_relocs, sizeof(uint32_t), 1, f);
    fwrite(&props->rodata_rel_relocs, sizeof(uint32_t), 1, f);
    fwrite(&props->data_abs_relocs, sizeof(uint32_t), 1, f);
    fwrite(&props->data_rel_relocs, sizeof(uint32_t), 1, f);

    fclose(f);
    // Additional writing of segments/reloc entries can be added as needed
}

// Example usage:
// int main() {
//     ThreeDSXProperties props;
//     read_3dsx("example.3dsx", &props);
//     print_properties(&props);
//     write_3dsx("output.3dsx", &props);
//     return 0;
// }