Skip to content

Commit 8f5ec4b

Browse files
committed
init
0 parents  commit 8f5ec4b

File tree

6 files changed

+478
-0
lines changed

6 files changed

+478
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__
2+
**.dat
3+
.DS_Store

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# pldb-parser
2+
3+
Parsing the proprietary IFF files that Reason uses to store plugin data. The format is used for other things, like a few different types of patches, but this parser is specifically for the PluginDatabase.dat file.
4+
5+
This is very proof-of-concept, I might make it more useful/stable in the future.
6+
7+
## usage
8+
9+
- Find your PluginDatabase.dat at `~/Library/Application Support/Propellerhead Software/Reason/Caches` (grab the newest one)
10+
- Copy to the work dir, rename to `pldb.dat`
11+
- Run `python pldb.py`
12+
- Read the output. Pipe it to a file if you wish. The world is your oyster.
13+
14+
## uad_plugin_sync
15+
16+
This script leverages some of the research I did into the UAD Console's internal TCP message protocol, [more on that here.](https://github.com/ebai101/UADCtrl.spoon) It finds any UAD plugins that aren't authorized and disables them in the PluginDatabase.dat file.
17+
18+
Usage is very similar, just run `python uad_plugin_sync.py` after copying your PluginDatabase.dat to the work dir. You need to rename the file back to its original name and copy it back to the original folder after the script has run. Obviously, you'll need to be connected to a UAD device for this to work.
19+
20+
## some notes on the file format
21+
22+
I got some good info from the NN-XT Patch Format, which is similar to the plugin database format. [Download that here.](https://cdn.reasonstudios.com/developers/NNXT/NN-XT_Patch_File_Format.zip) It seems to be an extension of the generic IFF container format with a few quirks.
23+
24+
The version tag in particular is specific to this format, which has a specific 5 byte sequence to represent the version of the stored data. For example:
25+
26+
| byte | value |
27+
| ---- | -------- |
28+
| 0xbc | reserved |
29+
| 0x01 | major |
30+
| 0x00 | minor |
31+
| 0x02 | revision |
32+
| 0x00 | reserved |
33+
34+
would be parsed as `1.0.2`.
35+
36+
The `enabled` and `crashed` fields were of particular interest to me. `enabled` is set to 3 if the plugin is enabled, and 5 if it is disabled. `crashed` is set to 1 if the plugin loads successfully, and 2 if not. Notably the plugin properties chunk (`PLPR`) does not generate if the plugin crashes on load.
37+
38+
There are a few data fields I'm not sure about, and they didn't seem necessary for what I was trying to do, so I just labeled them "unknown" of some sort. I'm not sure if the major and minor version tags are correct, either.
39+
40+
There's also `pldb_format.tcl`, which is a Hex Fiend template I built for reading these files in Hex Fiend. You can use that for more granular editing of the database.

chunk_reader.py

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Originally part of the Python standard library, chunk.py was deprecated in 3.11.
2+
# I need it for this project, so for future compatibility I'm including the file.
3+
# I take no credit for this code, I just renamed it to chunk_reader.py.
4+
5+
"""
6+
Simple class to read IFF chunks.
7+
8+
An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
9+
Format)) has the following structure:
10+
11+
+----------------+
12+
| ID (4 bytes) |
13+
+----------------+
14+
| size (4 bytes) |
15+
+----------------+
16+
| data |
17+
| ... |
18+
+----------------+
19+
20+
The ID is a 4-byte string which identifies the type of chunk.
21+
22+
The size field (a 32-bit value, encoded using big-endian byte order)
23+
gives the size of the whole chunk, including the 8-byte header.
24+
25+
Usually an IFF-type file consists of one or more chunks. The proposed
26+
usage of the Chunk class defined here is to instantiate an instance at
27+
the start of each chunk and read from the instance until it reaches
28+
the end, after which a new instance can be instantiated. At the end
29+
of the file, creating a new instance will fail with an EOFError
30+
exception.
31+
32+
Usage:
33+
while True:
34+
try:
35+
chunk = Chunk(file)
36+
except EOFError:
37+
break
38+
chunktype = chunk.getname()
39+
while True:
40+
data = chunk.read(nbytes)
41+
if not data:
42+
pass
43+
# do something with data
44+
45+
The interface is file-like. The implemented methods are:
46+
read, close, seek, tell, isatty.
47+
Extra methods are: skip() (called by close, skips to the end of the chunk),
48+
getname() (returns the name (ID) of the chunk)
49+
50+
The __init__ method has one required argument, a file-like object
51+
(including a chunk instance), and one optional argument, a flag which
52+
specifies whether or not chunks are aligned on 2-byte boundaries. The
53+
default is 1, i.e. aligned.
54+
"""
55+
56+
57+
class Chunk:
58+
def __init__(self, file, align=True, bigendian=True, inclheader=False):
59+
import struct
60+
61+
self.closed = False
62+
self.align = align # whether to align to word (2-byte) boundaries
63+
if bigendian:
64+
strflag = ">"
65+
else:
66+
strflag = "<"
67+
self.file = file
68+
self.chunkname = file.read(4)
69+
if len(self.chunkname) < 4:
70+
raise EOFError
71+
try:
72+
self.chunksize = struct.unpack_from(strflag + "L", file.read(4))[0]
73+
except struct.error:
74+
raise EOFError from None
75+
if inclheader:
76+
self.chunksize = self.chunksize - 8 # subtract header
77+
self.size_read = 0
78+
try:
79+
self.offset = self.file.tell()
80+
except (AttributeError, OSError):
81+
self.seekable = False
82+
else:
83+
self.seekable = True
84+
85+
def getname(self):
86+
"""Return the name (ID) of the current chunk."""
87+
return self.chunkname
88+
89+
def getsize(self):
90+
"""Return the size of the current chunk."""
91+
return self.chunksize
92+
93+
def close(self):
94+
if not self.closed:
95+
try:
96+
self.skip()
97+
finally:
98+
self.closed = True
99+
100+
def isatty(self):
101+
if self.closed:
102+
raise ValueError("I/O operation on closed file")
103+
return False
104+
105+
def seek(self, pos, whence=0):
106+
"""Seek to specified position into the chunk.
107+
Default position is 0 (start of chunk).
108+
If the file is not seekable, this will result in an error.
109+
"""
110+
111+
if self.closed:
112+
raise ValueError("I/O operation on closed file")
113+
if not self.seekable:
114+
raise OSError("cannot seek")
115+
if whence == 1:
116+
pos = pos + self.size_read
117+
elif whence == 2:
118+
pos = pos + self.chunksize
119+
if pos < 0 or pos > self.chunksize:
120+
raise RuntimeError
121+
self.file.seek(self.offset + pos, 0)
122+
self.size_read = pos
123+
124+
def tell(self):
125+
if self.closed:
126+
raise ValueError("I/O operation on closed file")
127+
return self.size_read
128+
129+
def read(self, size=-1):
130+
"""Read at most size bytes from the chunk.
131+
If size is omitted or negative, read until the end
132+
of the chunk.
133+
"""
134+
135+
if self.closed:
136+
raise ValueError("I/O operation on closed file")
137+
if self.size_read >= self.chunksize:
138+
return b""
139+
if size < 0:
140+
size = self.chunksize - self.size_read
141+
if size > self.chunksize - self.size_read:
142+
size = self.chunksize - self.size_read
143+
data = self.file.read(size)
144+
self.size_read = self.size_read + len(data)
145+
if self.size_read == self.chunksize and self.align and (self.chunksize & 1):
146+
dummy = self.file.read(1)
147+
self.size_read = self.size_read + len(dummy)
148+
return data
149+
150+
def skip(self):
151+
"""Skip the rest of the chunk.
152+
If you are not interested in the contents of the chunk,
153+
this method should be called so that the file points to
154+
the start of the next chunk.
155+
"""
156+
157+
if self.closed:
158+
raise ValueError("I/O operation on closed file")
159+
if self.seekable:
160+
try:
161+
n = self.chunksize - self.size_read
162+
# maybe fix alignment
163+
if self.align and (self.chunksize & 1):
164+
n = n + 1
165+
self.file.seek(n, 1)
166+
self.size_read = self.size_read + n
167+
return
168+
except OSError:
169+
pass
170+
while self.size_read < self.chunksize:
171+
n = min(8192, self.chunksize - self.size_read)
172+
dummy = self.read(n)
173+
if not dummy:
174+
raise EOFError

pldb.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import struct
2+
from pprint import pprint
3+
from chunk_reader import Chunk
4+
5+
6+
infile = "pldb.dat"
7+
out_data = {"plugins": []}
8+
9+
10+
def data_version(c):
11+
data = c.read(5)
12+
version = [
13+
int.from_bytes(n, byteorder="big") for n in struct.unpack(">x1c1c1cx", data)
14+
]
15+
return f"{version[0]}.{version[1]}.{version[2]}"
16+
17+
18+
def readstr(c):
19+
size = struct.unpack(">I", c.read(4))[0]
20+
string = struct.unpack(f">{size}s", c.read(size))[0].decode("utf-8")
21+
return string
22+
23+
24+
def parse(f):
25+
outer_form = Chunk(f)
26+
out_data["db size"] = outer_form.getsize()
27+
28+
outer_form.seek(4)
29+
pldv = Chunk(outer_form)
30+
out_data["data_version"] = data_version(pldv)
31+
outer_form.seek(pldv.tell() + 12)
32+
33+
for x in range(988):
34+
try:
35+
c = Chunk(outer_form)
36+
except EOFError:
37+
break
38+
c.seek(4)
39+
plugin = {}
40+
41+
plck = Chunk(c)
42+
plugin["plck"] = {}
43+
plugin["plck"]["data_version"] = data_version(plck)
44+
plugin["plck"]["filename"] = readstr(plck)
45+
plck.seek(4, whence=1)
46+
plugin["plck"]["unknown_tag_1"] = hex(struct.unpack(">H", plck.read(2))[0])
47+
plck.seek(4, whence=1)
48+
plugin["plck"]["unknown_tag_2"] = hex(struct.unpack(">H", plck.read(2))[0])
49+
plugin["plck"]["enabled"] = struct.unpack(">I", plck.read(4))[0]
50+
plugin["plck"]["enabled_offset"] = outer_form.tell() + 4
51+
plugin["plck"]["crashed"] = struct.unpack(">I", plck.read(4))[0]
52+
plck.skip()
53+
54+
if plugin["plck"]["crashed"] == 2:
55+
continue
56+
plpr = Chunk(c)
57+
plugin["plpr"] = {}
58+
plugin["plpr"]["data_version"] = data_version(plpr)
59+
plpr.seek(1, whence=1)
60+
plugin["plpr"]["name"] = readstr(plpr)
61+
plugin["plpr"]["manufacturer"] = readstr(plpr)
62+
plugin["plpr"]["min_version"] = readstr(plpr)
63+
plugin["plpr"]["maj_version"] = readstr(plpr)
64+
plpr.seek(4, whence=1)
65+
plugin["plpr"]["vst_id"] = readstr(plpr)
66+
plpr.seek(2, whence=1)
67+
plugin["plpr"]["categories"] = []
68+
num_categories = int.from_bytes(plpr.read(1), byteorder="big")
69+
for i in range(num_categories):
70+
plugin["plpr"]["categories"].append(readstr(plpr))
71+
plpr.skip()
72+
73+
out_data["plugins"].append(plugin)
74+
75+
return out_data
76+
77+
78+
if __name__ == "__main__":
79+
with open(infile, "rb") as f:
80+
data = parse(f)
81+
pprint(data)
82+
# print("disabled plugins")
83+
# pprint(
84+
# [x["plpr"]["name"] for x in data["plugins"] if x["plck"]["enabled"] != 3]
85+
# )

0 commit comments

Comments
 (0)