Task 272: .GSF File Format

Task 272: .GSF File Format

1. List of all the properties of this file format intrinsic to its file system

The .GSF (Game Boy Advance Sound Format) is a sub-format of the Portable Sound Format (PSF) family, designed for emulated audio from Game Boy Advance games. It encapsulates compressed ARM assembly code and data that, when emulated, reproduces the game's audio output. The file structure is binary and consists of a fixed header followed by variable sections. All multi-byte values are little-endian. The intrinsic properties (core structural elements defining the file system) are as follows:

  • Signature: 3 bytes, ASCII "PSF" (fixed identifier for all PSF variants).
  • Version: 1 byte, 0x22 (hex 22, decimal 34) for GSF (distinguishes it from other PSF variants like PSF1=0x01 or PSF2=0x02).
  • Reserved size (R): 4 bytes, unsigned 32-bit integer (length of the optional reserved area in bytes; typically 0 for GSF, but can hold system-specific metadata like GBA ROM headers if present).
  • Compressed program size (N): 4 bytes, unsigned 32-bit integer (length of the zlib-compressed program data in bytes; the core audio code and data).
  • Compressed program CRC-32: 4 bytes, unsigned 32-bit integer (checksum of the compressed program data for integrity verification).
  • Reserved area: Variable length (R bytes), binary data (optional; often empty in GSF, but may include GBA cartridge header if R > 0; treated as opaque binary).
  • Compressed program: Variable length (N bytes), zlib-compressed binary data (the executable ARM code for GBA audio emulation; decompression yields the raw program, typically starting with GBA ROM header at offset 0).
  • Tag signature (optional): 5 bytes, ASCII "[TAG]" (marks the start of metadata section if present; absence indicates end of file after compressed program).
  • Tag data (optional): Variable length (remaining bytes after tag signature), uncompressed ASCII text (human-readable metadata like title, game, artist, length in seconds, etc., in key-value format; truncated at 50KB if longer).

These properties define the self-contained, emulatable nature of the format, with no further sub-structure beyond the decompressed program (which is GBA-specific but not part of the container spec).

3. Ghost blog embedded html javascript

Paste the following code into a Ghost blog post as an HTML card (use the "Code" card type for the script section). It creates a drag-and-drop zone that parses a dropped .GSF file and dumps the properties to a scrollable div below. Uses FileReader and DataView for parsing; zlib decompression is not performed (properties are header-only).

Drag and drop a .GSF file here to parse its properties.

4. Python class

import struct
import zlib  # Not used for dump, but available for decompression if needed

class GSFReader:
    def __init__(self, filename):
        self.filename = filename
        self.data = None
        self.signature = None
        self.version = None
        self.reserved_size = None
        self.compressed_size = None
        self.crc32 = None
        self.reserved_data = None
        self.compressed_program = None
        self.tag_signature = None
        self.tag_data = None

    def read(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        offset = 0
        self.signature = self.data[offset:offset+3].decode('ascii')
        offset += 3
        self.version = struct.unpack('<B', self.data[offset:offset+1])[0]
        offset += 1
        self.reserved_size, = struct.unpack('<I', self.data[offset:offset+4])
        offset += 4
        self.compressed_size, = struct.unpack('<I', self.data[offset:offset+4])
        offset += 4
        self.crc32, = struct.unpack('<I', self.data[offset:offset+4])
        offset += 4
        self.reserved_data = self.data[offset:offset+self.reserved_size]
        offset += self.reserved_size
        self.compressed_program = self.data[offset:offset+self.compressed_size]
        offset += self.compressed_size
        if offset < len(self.data):
            self.tag_signature = self.data[offset:offset+5].decode('ascii')
            offset += 5
            self.tag_data = self.data[offset:].decode('ascii', errors='ignore')
        print_properties(self)

    def write(self, output_filename):
        with open(output_filename, 'wb') as f:
            f.write(self.data)
        print(f"Wrote to {output_filename}")

def print_properties(gsf):
    print(f"Signature: {gsf.signature}")
    print(f"Version: 0x{gsf.version:02x} ({'GSF' if gsf.version == 0x22 else 'Unknown'})")
    print(f"Reserved size: {gsf.reserved_size} bytes")
    print(f"Compressed program size: {gsf.compressed_size} bytes")
    print(f"Compressed program CRC-32: 0x{gsf.crc32:08x}")
    print(f"Reserved area: {len(gsf.reserved_data)} bytes (binary)")
    print(f"Compressed program: {len(gsf.compressed_program)} bytes (zlib)")
    if gsf.tag_signature:
        print(f"Tag signature: {gsf.tag_signature}")
        print(f"Tag data:\n{gsf.tag_data}")
    else:
        print("No tags found.")

# Example usage
# reader = GSFReader('example.gsf')
# reader.read()
# reader.write('output.gsf')

5. Java class

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;

public class GSFReader {
    private String filename;
    private byte[] data;
    private String signature;
    private int version;
    private int reservedSize;
    private int compressedSize;
    private int crc32;
    private byte[] reservedData;
    private byte[] compressedProgram;
    private String tagSignature;
    private String tagData;

    public GSFReader(String filename) {
        this.filename = filename;
    }

    public void read() throws IOException {
        data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int offset = 0;

        // Signature
        byte[] sigBytes = new byte[3];
        buffer.get(sigBytes, offset, 3);
        signature = new String(sigBytes);
        offset += 3;

        // Version
        version = buffer.get(offset) & 0xFF;
        offset += 1;

        // Reserved size
        reservedSize = buffer.getInt(offset);
        offset += 4;

        // Compressed size
        compressedSize = buffer.getInt(offset);
        offset += 4;

        // CRC32
        crc32 = buffer.getInt(offset);
        offset += 4;

        // Reserved data
        reservedData = new byte[reservedSize];
        buffer.get(reservedData, offset, reservedSize);
        offset += reservedSize;

        // Compressed program
        compressedProgram = new byte[compressedSize];
        buffer.get(compressedProgram, offset, compressedSize);
        offset += compressedSize;

        // Tags
        if (offset < data.length) {
            byte[] tagBytes = new byte[5];
            buffer.get(tagBytes, offset, 5);
            tagSignature = new String(tagBytes);
            offset += 5;
            tagData = new String(data, offset, data.length - offset);
        }

        printProperties();
    }

    public void write(String outputFilename) throws IOException {
        Files.write(Paths.get(outputFilename), data);
        System.out.println("Wrote to " + outputFilename);
    }

    private void printProperties() {
        System.out.println("Signature: " + signature);
        System.out.println("Version: 0x" + String.format("%02x", version) + " (" + (version == 0x22 ? "GSF" : "Unknown") + ")");
        System.out.println("Reserved size: " + reservedSize + " bytes");
        System.out.println("Compressed program size: " + compressedSize + " bytes");
        System.out.println("Compressed program CRC-32: 0x" + String.format("%08x", crc32));
        System.out.println("Reserved area: " + reservedData.length + " bytes (binary)");
        System.out.println("Compressed program: " + compressedProgram.length + " bytes (zlib)");
        if (tagSignature != null) {
            System.out.println("Tag signature: " + tagSignature);
            System.out.println("Tag data:\n" + tagData);
        } else {
            System.out.println("No tags found.");
        }
    }

    // Example usage
    // GSFReader reader = new GSFReader("example.gsf");
    // reader.read();
    // reader.write("output.gsf");
}

6. Javascript class

class GSFReader {
  constructor(fileOrBuffer) {
    this.data = null;
    this.signature = null;
    this.version = null;
    this.reservedSize = null;
    this.compressedSize = null;
    this.crc32 = null;
    this.reservedData = null;
    this.compressedProgram = null;
    this.tagSignature = null;
    this.tagData = null;

    if (fileOrBuffer instanceof File) {
      const reader = new FileReader();
      reader.onload = (e) => {
        this.data = new Uint8Array(e.target.result);
        this.parse();
        this.printProperties();
      };
      reader.readAsArrayBuffer(fileOrBuffer);
    } else if (fileOrBuffer instanceof ArrayBuffer) {
      this.data = new Uint8Array(fileOrBuffer);
      this.parse();
    }
  }

  parse() {
    const view = new DataView(this.data.buffer);
    let offset = 0;

    // Signature
    this.signature = String.fromCharCode(
      view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2)
    );
    offset += 3;

    // Version
    this.version = view.getUint8(offset);
    offset += 1;

    // Reserved size
    this.reservedSize = view.getUint32(offset, true);
    offset += 4;

    // Compressed size
    this.compressedSize = view.getUint32(offset, true);
    offset += 4;

    // CRC32
    this.crc32 = view.getUint32(offset, true);
    offset += 4;

    // Reserved data
    this.reservedData = this.data.slice(offset, offset + this.reservedSize);
    offset += this.reservedSize;

    // Compressed program
    this.compressedProgram = this.data.slice(offset, offset + this.compressedSize);
    offset += this.compressedSize;

    // Tags
    if (offset < this.data.length) {
      this.tagSignature = String.fromCharCode(
        view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2),
        view.getUint8(offset + 3), view.getUint8(offset + 4)
      );
      offset += 5;
      const decoder = new TextDecoder();
      this.tagData = decoder.decode(this.data.subarray(offset));
    }
  }

  write(filename) {
    const blob = new Blob([this.data]);
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(url);
    console.log(`Wrote to ${filename} (browser download)`);
  }

  printProperties() {
    console.log(`Signature: ${this.signature}`);
    console.log(`Version: 0x${this.version.toString(16).padStart(2, '0')} (${this.version === 0x22 ? 'GSF' : 'Unknown'})`);
    console.log(`Reserved size: ${this.reservedSize} bytes`);
    console.log(`Compressed program size: ${this.compressedSize} bytes`);
    console.log(`Compressed program CRC-32: 0x${this.crc32.toString(16).padStart(8, '0')}`);
    console.log(`Reserved area: ${this.reservedData.length} bytes (binary)`);
    console.log(`Compressed program: ${this.compressedProgram.length} bytes (zlib)`);
    if (this.tagSignature) {
      console.log(`Tag signature: ${this.tagSignature}`);
      console.log(`Tag data:\n${this.tagData}`);
    } else {
      console.log('No tags found.');
    }
  }
}

// Example usage (browser console)
// const reader = new GSFReader(fileFromInput);
// reader.write('output.gsf');

7. C class

(Note: C doesn't have classes, so this is a struct with functions. Compile with gcc gsf.c -o gsf -lz for zlib support, though not used for dump.)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <zlib.h>  // Optional for decompression

typedef struct {
    char filename[256];
    uint8_t* data;
    size_t data_size;
    char signature[4];  // Null-terminated
    uint8_t version;
    uint32_t reserved_size;
    uint32_t compressed_size;
    uint32_t crc32;
    uint8_t* reserved_data;
    uint8_t* compressed_program;
    char tag_signature[6];  // Null-terminated
    char* tag_data;
} GSFReader;

void gsf_read(GSFReader* gsf) {
    FILE* f = fopen(gsf->filename, "rb");
    if (!f) {
        perror("Error opening file");
        return;
    }
    fseek(f, 0, SEEK_END);
    gsf->data_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    gsf->data = malloc(gsf->data_size);
    fread(gsf->data, 1, gsf->data_size, f);
    fclose(f);

    uint8_t* ptr = gsf->data;
    memcpy(gsf->signature, ptr, 3);
    gsf->signature[3] = '\0';
    ptr += 3;

    gsf->version = *ptr;
    ptr += 1;

    gsf->reserved_size = *(uint32_t*)ptr;
    ptr += 4;

    gsf->compressed_size = *(uint32_t*)ptr;
    ptr += 4;

    gsf->crc32 = *(uint32_t*)ptr;
    ptr += 4;

    gsf->reserved_data = malloc(gsf->reserved_size);
    memcpy(gsf->reserved_data, ptr, gsf->reserved_size);
    ptr += gsf->reserved_size;

    gsf->compressed_program = malloc(gsf->compressed_size);
    memcpy(gsf->compressed_program, ptr, gsf->compressed_size);
    ptr += gsf->compressed_size;

    if (ptr < gsf->data + gsf->data_size) {
        memcpy(gsf->tag_signature, ptr, 5);
        gsf->tag_signature[5] = '\0';
        ptr += 5;
        gsf->tag_data = malloc(gsf->data_size - (ptr - gsf->data) + 1);
        memcpy(gsf->tag_data, ptr, gsf->data_size - (ptr - gsf->data));
        gsf->tag_data[gsf->data_size - (ptr - gsf->data)] = '\0';
    }

    print_properties(gsf);
}

void gsf_write(GSFReader* gsf, const char* output_filename) {
    FILE* f = fopen(output_filename, "wb");
    if (!f) {
        perror("Error writing file");
        return;
    }
    fwrite(gsf->data, 1, gsf->data_size, f);
    fclose(f);
    printf("Wrote to %s\n", output_filename);
}

void print_properties(GSFReader* gsf) {
    printf("Signature: %s\n", gsf->signature);
    printf("Version: 0x%02x (%s)\n", gsf->version, gsf->version == 0x22 ? "GSF" : "Unknown");
    printf("Reserved size: %u bytes\n", gsf->reserved_size);
    printf("Compressed program size: %u bytes\n", gsf->compressed_size);
    printf("Compressed program CRC-32: 0x%08x\n", gsf->crc32);
    printf("Reserved area: %zu bytes (binary)\n", gsf->reserved_size);
    printf("Compressed program: %zu bytes (zlib)\n", gsf->compressed_size);
    if (gsf->tag_signature[0]) {
        printf("Tag signature: %s\n", gsf->tag_signature);
        printf("Tag data:\n%s\n", gsf->tag_data);
    } else {
        printf("No tags found.\n");
    }
}

void gsf_free(GSFReader* gsf) {
    free(gsf->data);
    free(gsf->reserved_data);
    free(gsf->compressed_program);
    free(gsf->tag_data);
}

// Example usage
// GSFReader gsf = { .filename = "example.gsf" };
// gsf_read(&gsf);
// gsf_write(&gsf, "output.gsf");
// gsf_free(&gsf);