Parse mi files (#935)

* Add more maki files found online

* Add parser for .mi files and confirm it can derive the objects we have today
This commit is contained in:
Jordan Eldredge 2019-09-28 14:50:35 -07:00
parent 1215beec76
commit 460ea396ec
6 changed files with 2822 additions and 0 deletions

View file

@ -0,0 +1,189 @@
Nullsoft Database Engine Format Specifications v1.0
---------------------------------------------------
1. Tables
---------
Tables are saved using two files, one containing the data, and one containing the indexes :
Data file : <filename>.DAT
Index file : <filename>.IDX
2. Index File Structure
-----------------------
Note that string types are not zero terminated, they are either of fixed size (indicated in the following tables)
or their size is determined by another field.
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 STRING 8 "NDEINDEX"
8 INT 4 Number of records
12 INDEX 4*NumberOfRecords Primary Index (insertion order)
Optional :
4*NumberOfRecords+12 INDEX 4*NumberOfRecords First Secondary Index
2*(4*NumberOfRecords)+12 INDEX 4*NumberOfRecords Second Secondary Index
...
==================================================================================================
The INDEX type is a simple array of INT (4 bytes) where each entry represents the offset of the record in the datafile.
Thus, INDEX[0] gives you the offset for the first record of that index.
3. Data File Structure
----------------------
The data file is a simple data pool from which structured data is pulled. The file begins with a 8 bytes long
string signature that reads "NDETABLE", there rest is the data.
Records are fetched directly from the data pool using the offset stored in each appropriate index, using the
beginning of the file as offset 0.
4. Records Structure
--------------------
A record is a linked list made of 'field' elements, and so, when you retrieve a record, you actually retrieve the
first field of a linked list, and then add each new field to the record structure (or class) as you read more of them,
following the chain.
Record 0 is a list of the columns that make our table.
Record 1 is a list of the indexes that are found in the index file.
Record 2 is the first record of the table, it is a list of fields of various types (ie, int, string, etc)
5. Fields Structures
--------------------
The following structures indicate offsets relative to the beginning of the (sub)structures.
Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 UCHAR 1 Column ID
1 UCHAR 1 Field Type
2 INT 4 Size of field data
6 INT 4 Next field position in table data pool
10 INT 4 Previous field position in table data pool
14 FIELDDATA SizeOfFieldData Field data
==================================================================================================
Field Types :
FIELD_UNDEFINED = 255
FIELD_COLUMN = 0
FIELD_INDEX = 1
FIELD_REDIRECTOR = 2
FIELD_STRING = 3
FIELD_INTEGER = 4
FIELD_BOOLEAN = 5
FIELD_BINARY = 6
FIELD_GUID = 7
FIELD_FLOAT = 9
FIELD_DATETIME = 10
FIELD_LENGTH = 11
Field Data Types :
* Column Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 UCHAR 1 Column Field Type (ie, FIELD_INTEGER)
1 UCHAR 1 Index unique values (0/1)
2 UCHAR 1 Size of column name string
3 STRING SizeOfColumnName Public name of the column
==================================================================================================
* Index Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 INT 4 Offset of this index in the index file
4 INT 4 Type of the field on which this index sorts (FIELD_UNDEFINED for the primary index)
8 UCHAR 1 Size of index name string
9 STRING SizeOfIndexName Public name of the index
==================================================================================================
* Redirector Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 INT 4 Redirection offset in the data pool
==================================================================================================
If you read a redirector field, you should move to the appropriate offset and read a new field there.
* String Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 USHORT 2 Size of string
2 STRING SizeOfString String
==================================================================================================
* Integer Field, DateTime Field, Length Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 INT 4 Integer value
==================================================================================================
Length fields are exactly the same as integer fields, but instead of holding some unknown quantity, they have been
marked as lengths by the application.
For DateTime fields, the value is to be interpreted as a standard C time_t value.
* Boolean Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 UCHAR 1 Boolean value
==================================================================================================
* Binary Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 USHORT 2 Size of binary data
2 BINARY SizeOfBinaryData Binary data
==================================================================================================
* GUID Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 GUID 16 Guid data
==================================================================================================
* Float Field :
==================================================================================================
Offset Data Type Size Field
==================================================================================================
0 FLOAT 4 Float value
==================================================================================================

View file

@ -0,0 +1,22 @@
#ifndef __CONFIG_MI
#define __CONFIG_MI
extern class @{593DBA22 - D077 - 4976 - B952 - F4713655400B} @Object _predecl Config;
extern class @{D4030282 - 3AAB - 4d87 - 878D - 12326FADFCD5} @Object ConfigItem;
extern class @{24DEC283 - B76E - 4a36 - 8CCC - 9E24C46B6C73} @Object ConfigAttribute;
extern ConfigItem Config.getItem(String item_name);
extern ConfigItem Config.getItemByGuid(String item_guid);
extern ConfigItem Config.newItem(String item_name, String item_guid);
extern ConfigAttribute ConfigItem.getAttribute(String attr_name);
extern ConfigAttribute ConfigItem.newAttribute(String attr_name, String default_val);
extern String ConfigItem.getGuid(String attr_name);
extern ConfigAttribute.setData(String value);
extern String ConfigAttribute.getData();
extern ConfigAttribute.onDataChanged();
extern ConfigItem ConfigAttribute.getParentItem();
extern String ConfigAttribute.getAttributeName();
#endif

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
Scraped from https://web.archive.org/web/20051122102353/http://bluemars.planet-d.net/sdk/ wich was discovered via a link in Objects.pm from the decompiler.
The server had a number of binary files as well, which sadly the Internet Archvie did not capture.
wasabi-sdk500-b3.exe
wasabi-sdk499g8.exe
maki/mc.exe

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Based on extract_objects.pl from Ralf Engels<ralf.engels@gmx.de> Maki decompiler
*/
const fs = require("fs");
function parseFile(filePath) {
const mi = fs.readFileSync(filePath, "utf8");
const lines = mi.split("\n");
const objects = {};
lines.forEach((line, lineNumber) => {
const classDefinitionMatch = /\s*extern\s+class\s*\@\{(........\s?-\s?....\s?-\s?....\s?-\s?....\s?-\s?............)\}\s*\@\s*(.*?)\s+(.*?);/.exec(
line
);
if (classDefinitionMatch) {
const id = classDefinitionMatch[1].replace(/[-\s]/g, "");
const parent = classDefinitionMatch[2];
const name = classDefinitionMatch[3]
.replace(/^_predecl /, "")
.replace(/^&/, "");
objects[name.toLowerCase()] = { id, name, parent, functions: [] };
}
const methodMatch = /\s*extern(\s+.*)?\s+(.*)\.(.*)\((.*)\);/.exec(line);
if (methodMatch) {
const result = methodMatch[1] == null ? "" : methodMatch[1].trim();
const className = methodMatch[2].toLowerCase();
const name = methodMatch[3];
const rawArgs = methodMatch[4].split(/\s*,\s*/);
const parameters = rawArgs.filter(Boolean).map(rawArg => {
const argMatch = /^\s*(.*\s+)?(.*)\s*/.exec(rawArg);
if (argMatch == null) {
throw new Error(`Could not find args in ${rawArg} in ${line}`);
}
const type = argMatch[1];
if (type == null) {
// console.warn(`Could not find args name in ${fileName}:${lineNum} "${line}"`);
return [argMatch[2], "unknown_arg_name"];
}
return [type.trim(), argMatch[2]];
});
if (objects[className] == null) {
throw new Error(
`"${className} not defined in ${filePath}:${lineNumber}. I have ${JSON.stringify(
Object.keys(objects)
)}`
);
}
objects[className].functions.push({ result, name, parameters });
}
});
const objectIds = {};
Object.keys(objects).forEach(normalizedName => {
const { id, parent, functions, name } = objects[normalizedName];
objectIds[id] = { parent, functions, name };
});
return objectIds;
}
module.exports = { parseFile };

View file

@ -0,0 +1,66 @@
import { objects } from "../objects";
import { parseFile } from "../tools/parse-mi";
import path from "path";
// Between myself and the author of the decompiler, a number of manual tweaks
// have been made to our current object definitions. This function recreates
// those tweaks so we can have an apples to apples comparison.
function applyPatches(parsedObjects) {
/*
* From object.js
*
* > The std.mi has this set as void, but we checked in Winamp and confirmed it
* > returns 0/1
*/
parsedObjects["5D0C5BB67DE14b1fA70F8D1659941941"].functions[5].result =
"boolean";
/*
* From Object.pm
*
* > note, std.mi does not have this parameter!
*/
parsedObjects.B4DCCFFF81FE4bcc961B720FD5BE0FFF.functions[0].parameters[0][1] =
"onoff";
/*
* From Object.pm
*
* > note, my std.mi did not contain this!
*/
parsedObjects.B4DCCFFF81FE4bcc961B720FD5BE0FFF.functions.push({
parameters: [],
name: "getCurCfgVal",
result: "Int",
});
}
// In order to do traversal objects.js adds parent refrences.
// For comparison, we don't want these.
function orphan(objs) {
const newObjs = {};
Object.keys(objs).forEach(key => {
const { parentClass, ...rest } = objs[key];
newObjs[key] = rest;
});
return newObjs;
}
test("parseFile()", () => {
const compilers = path.join(__dirname, "../../../resources/maki_compiler/");
const lib30full = path.join(compilers, "v1.1.1.b3 (Winamp 3.0 full)/Lib/");
const lib502 = path.join(compilers, "v1.1.13 (Winamp 5.02)/lib/");
// const lib566 = path.join(compilers, "v1.2.0 (Winamp 5.66)/lib/");
const libUnknown = path.join(compilers, "Unknown (Winamp 5.03)/lib/");
const parsedObjects = {
...parseFile(path.join(lib502, "std.mi")),
...parseFile(path.join(lib30full, "pldir.mi")),
...parseFile(path.join(libUnknown, "config.mi")),
};
applyPatches(parsedObjects);
expect(parsedObjects).toEqual(orphan(objects));
});