Task 728: .THM File Format

Task 728: .THM File Format

File Format Specifications for .THM (Sony Ericsson Theme File)

The .THM file format is used for Sony Ericsson mobile phone themes. It is a container format that bundles an XML configuration file and associated media files (such as images and sounds) in a concatenated structure with a custom header, essentially a tar archive prefixed with metadata.

1. List of All Properties Intrinsic to the File Format

The properties intrinsic to the .THM file format include metadata from the header and the embedded theme configuration. Based on the format specification, the key properties are:

  • Original file name: A null-terminated string at offset 0x0000, up to 100 bytes.
  • Octal numbers list: A series of octal numbers (parsed from null or blank-terminated strings at offset 0x0064), representing various metadata, with the fourth number being the body length.
  • Body length: The length of the data body in bytes (fourth octal number).
  • Tar indicator string: "ustar" at offset 0x0101.
  • Group string: "nogroup" at offset 0x0129.
  • Data body: Starts at offset 0x0200, containing a tar archive with embedded files.
  • Embedded files: Typically includes 'Theme.xml' and image files (e.g., .png, .jpg, .gif).
  • Theme version: From the XML, the 'version' attribute in the <Sony_Ericsson_theme> tag.
  • Background color: Color value in the  tag.
  • Background image source: Source attribute in the <Background_image> tag.
  • Desktop color: Color value in the  tag.
  • Padding: Null bytes after the body until the next entry or end of file.

These properties define the structure and content of the file, allowing for multiple concatenated entries if present.

3. Ghost Blog Embedded HTML JavaScript for Drag and Drop

The following is an HTML page with embedded JavaScript that allows users to drag and drop a .THM file. It parses the file and dumps all properties to the screen.

.THM File Property Dumper
Drag and drop a .THM file here

4. Python Class for .THM File

The following Python class can open, decode, read, write, and print the properties of a .THM file.

import struct
import tarfile
from io import BytesIO
import xml.etree.ElementTree as ET

class ThmFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = None
        self.properties = {}

    def open(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()

    def decode(self):
        if not self.data:
            raise ValueError("File not opened")
        ofs = 0
        name = self._get_null_terminated_string(ofs, 100)
        ofs += 100
        numbers_str = self._get_null_terminated_string(ofs, 156)
        numbers = [int(n, 8) for n in numbers_str.split() if n.isdigit() and all(c in '01234567' for c in n)]
        body_len = numbers[3] if len(numbers) > 3 else 0
        ofs += 156
        ustar = self._get_string(ofs + 1, 5)
        nogroup = self._get_string(ofs + 40, 7)
        ofs += 156  # to 0x0200
        body = self.data[ofs:ofs + body_len]
        xml = self._extract_xml_from_tar(body)
        properties = self._parse_xml(xml)
        self.properties = {
            'Original File Name': name,
            'Octal Numbers': numbers,
            'Body Length': body_len,
            'Ustar String': ustar,
            'Nogroup String': nogroup,
            'Embedded XML Properties': properties
        }

    def print_properties(self):
        print(self.properties)

    def write(self, new_filepath):
        if not self.data:
            raise ValueValueError("No data to write")
        with open(new_filepath, 'wb') as f:
            f.write(self.data)

    def _get_null_terminated_string(self, start, max_len):
        end = self.data.find(b'\0', start, start + max_len)
        return self.data[start:end].decode('utf-8').trim()

    def _get_string(self, start, len_):
        return self.data[start:start + len_].decode('utf-8').trim()

    def _extract_xml_from_tar(self, body):
        with BytesIO(body) as bio:
            with tarfile.open(fileobj=bio) as tar:
                for member in tar:
                    if member.name == 'Theme.xml':
                        return tar.extractfile(member).read().decode('utf-8')
        return ''

    def _parse_xml(self, xml_str):
        properties = {}
        if xml_str:
            root = ET.fromstring(xml_str)
            for elem in root.iter():
                if elem.attrib:
                    for k, v in elem.attrib.items():
                        properties[f'{elem.tag} {k}'] = v
                if elem.text and elem.text.strip():
                    properties[elem.tag] = elem.text.strip()
        return properties

5. Java Class for .THM File

The following Java class can open, decode, read, write, and print the properties of a .THM file.

import java.io.*;
import java.util.*;
import java.util.regex.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilderFactory;

public class ThmFile {
    private String filepath;
    private byte[] data;
    private Map<String, Object> properties = new HashMap<>();

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

    public void open() throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            data = fis.readAllBytes();
        }
    }

    public void decode() throws Exception {
        if (data == null) throw new IllegalStateException("File not opened");
        int ofs = 0;
        String name = getNullTerminatedString(ofs, 100);
        ofs += 100;
        String numbersStr = getNullTerminatedString(ofs, 156);
        List<Integer> numbers = new ArrayList<>();
        Matcher m = Pattern.compile("[0-7]+").matcher(numbersStr);
        while (m.find()) {
            numbers.add(Integer.parseInt(m.group(), 8));
        }
        int bodyLen = numbers.size() > 3 ? numbers.get(3) : 0;
        ofs += 156;
        String ustar = getString(ofs + 1, 5);
        String nogroup = getString(ofs + 40, 7);
        ofs += 156;
        byte[] body = Arrays.copyOfRange(data, ofs, ofs + bodyLen);
        String xml = extractXmlFromTar(body);
        Map<String, String> xmlProperties = parseXml(xml);
        properties.put("Original File Name", name);
        properties.put("Octal Numbers", numbers);
        properties.put("Body Length", bodyLen);
        properties.put("Ustar String", ustar);
        properties.put("Nogroup String", nogroup);
        properties.put("Embedded XML Properties", xmlProperties);
    }

    public void printProperties() {
        System.out.println(properties);
    }

    public void write(String newFilepath) throws IOException {
        if (data == null) throw new IllegalStateException("No data to write");
        try (FileOutputStream fos = new FileOutputStream(newFilepath)) {
            fos.write(data);
        }
    }

    private String getNullTerminatedString(int start, int maxLen) {
        int end = start;
        while (end < start + maxLen && data[end] != 0) end++;
        return new String(data, start, end - start).trim();
    }

    private String getString(int start, int len) {
        return new String(data, start, len).trim();
    }

    private String extractXmlFromTar(byte[] body) throws IOException {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(body)) {
            try (BufferedInputStream bis = new BufferedInputStream(bais)) {
                int tarOfs = 0;
                while (tarOfs < body.length) {
                    String name = getNullTerminatedString(tarOfs, 100);
                    if (name.equals("Theme.xml")) {
                        String sizeStr = getString(tarOfs + 124, 11);
                        int size = Integer.parseInt(sizeStr.trim(), 8);
                        return getString(tarOfs + 512, size);
                    }
                    String sizeStr = getString(tarOfs + 124, 11);
                    int size = Integer.parseInt(sizeStr.trim(), 8);
                    tarOfs += 512 + ((size + 511) / 512) * 512;
                }
            }
        }
        return "";
    }

    private Map<String, String> parseXml(String xml) throws Exception {
        Map<String, String> props = new HashMap<>();
        if (!xml.isEmpty()) {
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes()));
            traverseNode(doc.getDocumentElement(), props);
        }
        return props;
    }

    private void traverseNode(Element node, Map<String, String> props) {
        if (node.hasAttributes()) {
            for (int i = 0; i < node.getAttributes().getLength(); i++) {
                props.put(node.getTagName() + " " + node.getAttributes().item(i).getNodeName(), node.getAttributes().item(i).getNodeValue());
            }
        }
        if (node.getTextContent() != null && !node.getTextContent().trim().isEmpty()) {
            props.put(node.getTagName(), node.getTextContent().trim());
        }
        for (int i = 0; i < node.getChildNodes().getLength(); i++) {
            if (node.getChildNodes().item(i) instanceof Element) {
                traverseNode((Element) node.getChildNodes().item(i), props);
            }
        }
    }
}

6. JavaScript Class for .THM File

The following JavaScript class can open (using FileReader), decode, read, write (using Blob), and print the properties of a .THM file to console.

class ThmFile {
    constructor(file) {
        this.file = file;
        this.data = null;
        this.properties = {};
    }

    async open() {
        const buffer = await this.file.arrayBuffer();
        this.data = new Uint8Array(buffer);
    }

    parse() {
        if (!this.data) throw new Error("File not opened");
        let ofs = 0;
        const name = this.getNullTerminatedString(ofs, 100);
        ofs += 100;
        const numbersStr = this.getNullTerminatedString(ofs, 156);
        const numbers = numbersStr.match(/[0-7]+/g).map(n => parseInt(n, 8));
        const bodyLen = numbers[3] || 0;
        ofs += 156;
        const ustar = this.getString(ofs + 1, 5);
        const nogroup = this.getString(ofs + 40, 7);
        ofs += 156;
        const body = this.data.subarray(ofs, ofs + bodyLen);
        const xml = this.extractXmlFromTar(body);
        const properties = this.parseXml(xml);
        this.properties = {
            'Original File Name': name,
            'Octal Numbers': numbers,
            'Body Length': bodyLen,
            'Ustar String': ustar,
            'Nogroup String': nogroup,
            'Embedded XML Properties': properties
        };
    }

    printProperties() {
        console.log(this.properties);
    }

    write() {
        if (!this.data) throw new Error("No data to write");
        const blob = new Blob([this.data], {type: 'application/octet-stream'});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = this.file.name;
        a.click();
        URL.revokeObjectURL(url);
    }

    getNullTerminatedString(start, maxLen) {
        let end = start;
        while (end < start + maxLen && this.data[end] !== 0) end++;
        return new TextDecoder().decode(this.data.subarray(start, end)).trim();
    }

    getString(start, len) {
        return new TextDecoder().decode(this.data.subarray(start, start + len)).trim();
    }

    extractXmlFromTar(body) {
        let tarOfs = 0;
        while (tarOfs < body.length) {
            const name = this.getNullTerminatedString(tarOfs, 100);
            if (name === 'Theme.xml') {
                const sizeStr = this.getString(tarOfs + 124, 11);
                const size = parseInt(sizeStr, 8);
                return this.getString(tarOfs + 512, size);
            }
            const sizeStr = this.getString(tarOfs + 124, 11);
            const size = parseInt(sizeStr, 8);
            tarOfs += 512 + Math.ceil(size / 512) * 512;
        }
        return '';
    }

    parseXml(xml) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(xml, 'text/xml');
        const properties = {};
        const traverse = (node) => {
            if (node.attributes) {
                for (let attr of node.attributes) {
                    properties[`${node.tagName} ${attr.name}`] = attr.value;
                }
            }
            if (node.textContent.trim()) {
                properties[node.tagName] = node.textContent.trim();
            }
            for (let child of node.children) {
                traverse(child);
            }
        };
        traverse(doc.documentElement);
        return properties;
    }
}

7. C Class for .THM File

The following C class (struct with functions) can open, decode, read, write, and print the properties of a .THM file to console. Note: XML parsing is basic string-based due to no library.

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

typedef struct {
    char *filepath;
    unsigned char *data;
    long size;
    char *properties; // JSON-like string for properties
} ThmFile;

ThmFile* thm_new(const char *filepath) {
    ThmFile* thm = malloc(sizeof(ThmFile));
    thm->filepath = strdup(filepath);
    thm->data = NULL;
    thm->size = 0;
    thm->properties = NULL;
    return thm;
}

void thm_open(ThmFile *thm) {
    FILE *f = fopen(thm->filepath, "rb");
    if (f) {
        fseek(f, 0, SEEK_END);
        thm->size = ftell(f);
        fseek(f, 0, SEEK_SET);
        thm->data = malloc(thm->size);
        fread(thm->data, 1, thm->size, f);
        fclose(f);
    }
}

void thm_decode(ThmFile *thm) {
    if (!thm->data) return;
    int ofs = 0;
    char name[101] = {0};
    strncpy(name, (char*)thm->data + ofs, 100);
    ofs += 100;
    char numbersStr[157] = {0};
    strncpy(numbersStr, (char*)thm->data + ofs, 156);
    // Parse numbers (simplified)
    char *token = strtok(numbersStr, " \0");
    int numbers[10] = {0};
    int idx = 0;
    while (token && idx < 10) {
        numbers[idx++] = strtol(token, NULL, 8);
        token = strtok(NULL, " \0");
    }
    int bodyLen = numbers[3];
    ofs += 156;
    char ustar[6] = {0};
    strncpy(ustar, (char*)thm->data + ofs + 1, 5);
    char nogroup[8] = {0};
    strncpy(nogroup, (char*)thm->data + ofs + 40, 7);
    ofs += 156;
    unsigned char *body = thm->data + ofs;
    // Extract XML from tar (simplified, assume first file is XML)
    char tarName[101] = {0};
    strncpy(tarName, (char*)body, 100);
    char sizeStr[12] = {0};
    strncpy(sizeStr, (char*)body + 124, 11);
    int xmlSize = strtol(sizeStr, NULL, 8);
    char *xml = malloc(xmlSize + 1);
    strncpy(xml, (char*)body + 512, xmlSize);
    xml[xmlSize] = '\0';
    // Basic XML parse (find key values)
    char *version = strstr(xml, "version=\"");
    char *bgColor = strstr(xml, "<Background Color=\"");
    // Add more as needed
    // For print, format string
    char *props = malloc(1024);
    sprintf(props, "Original File Name: %s\nOctal Numbers: [%d, ...]\nBody Length: %d\nUstar: %s\nNogroup: %s\nXML: %s\n", name, numbers[0], bodyLen, ustar, nogroup, xml);
    thm->properties = props;
    free(xml);
}

void thm_print_properties(ThmFile *thm) {
    if (thm->properties) printf("%s", thm->properties);
}

void thm_write(ThmFile *thm, const char *new_filepath) {
    if (!thm->data) return;
    FILE *f = fopen(new_filepath, "wb");
    if (f) {
        fwrite(thm->data, 1, thm->size, f);
        fclose(f);
    }
}

void thm_free(ThmFile *thm) {
    free(thm->filepath);
    free(thm->data);
    free(thm->properties);
    free(thm);
}