sdbtool 0.3.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sdbtool-0.3.2 → sdbtool-0.4.0}/PKG-INFO +5 -3
- {sdbtool-0.3.2 → sdbtool-0.4.0}/README.md +4 -2
- {sdbtool-0.3.2 → sdbtool-0.4.0}/pyproject.toml +3 -2
- sdbtool-0.4.0/sdbtool/__init__.py +0 -0
- sdbtool-0.4.0/sdbtool/__main__.py +4 -0
- {sdbtool-0.3.2 → sdbtool-0.4.0}/sdbtool/apphelp/__init__.py +17 -6
- {sdbtool-0.3.2 → sdbtool-0.4.0}/sdbtool/apphelp/winapi.py +113 -0
- sdbtool-0.4.0/sdbtool/attributes.py +24 -0
- sdbtool-0.4.0/sdbtool/cli.py +53 -0
- sdbtool-0.4.0/sdbtool/sdb2xml.py +124 -0
- sdbtool-0.4.0/sdbtool/xml.py +60 -0
- sdbtool-0.3.2/sdbtool/__init__.py +0 -25
- sdbtool-0.3.2/sdbtool/__main__.py +0 -4
- sdbtool-0.3.2/sdbtool/cli.py +0 -14
- sdbtool-0.3.2/sdbtool/xml.py +0 -139
- {sdbtool-0.3.2 → sdbtool-0.4.0}/.github/workflows/python-publish.yml +0 -0
- {sdbtool-0.3.2 → sdbtool-0.4.0}/.github/workflows/python-test.yml +0 -0
- {sdbtool-0.3.2 → sdbtool-0.4.0}/.gitignore +0 -0
- {sdbtool-0.3.2 → sdbtool-0.4.0}/LICENSE.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdbtool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Dump SDB file to xml using apphelp.dll
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/sdbtool/
|
|
6
6
|
Project-URL: Changelog, https://github.com/learn-more/sdbtool/releases
|
|
@@ -35,6 +35,7 @@ A tool for converting Microsoft Application Compatibility Database (SDB) files t
|
|
|
35
35
|
|
|
36
36
|
- Parses SDB files used by Windows for application compatibility.
|
|
37
37
|
- Converts SDB data into readable XML.
|
|
38
|
+
- Dump file attributes in SDB-recognizable format
|
|
38
39
|
- Useful for analysis, migration, or documentation.
|
|
39
40
|
|
|
40
41
|
|
|
@@ -47,8 +48,9 @@ Sdbtool is available as [`sdbtool`](https://pypi.org/project/sdbtool/) on PyPI.
|
|
|
47
48
|
Invoke sdbtool directly with [`uvx`](https://docs.astral.sh/uv/):
|
|
48
49
|
|
|
49
50
|
```shell
|
|
50
|
-
uvx sdbtool your.sdb # Convert the file 'your.sdb' to xml, and print it to the console
|
|
51
|
-
uvx sdbtool your.sdb --output your.xml # Convert the file 'your.sdb' to xml, and write it to 'your.xml'
|
|
51
|
+
uvx sdbtool sdb2xml your.sdb # Convert the file 'your.sdb' to xml, and print it to the console
|
|
52
|
+
uvx sdbtool sdb2xml your.sdb --output your.xml # Convert the file 'your.sdb' to xml, and write it to 'your.xml'
|
|
53
|
+
uvx sdbtool attributes your.exe # Show the file attributes as recognized by apphelp in an XML-friendly format
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
Or install sdbtool with `uv` (recommended), `pip`, or `pipx`:
|
|
@@ -13,6 +13,7 @@ A tool for converting Microsoft Application Compatibility Database (SDB) files t
|
|
|
13
13
|
|
|
14
14
|
- Parses SDB files used by Windows for application compatibility.
|
|
15
15
|
- Converts SDB data into readable XML.
|
|
16
|
+
- Dump file attributes in SDB-recognizable format
|
|
16
17
|
- Useful for analysis, migration, or documentation.
|
|
17
18
|
|
|
18
19
|
|
|
@@ -25,8 +26,9 @@ Sdbtool is available as [`sdbtool`](https://pypi.org/project/sdbtool/) on PyPI.
|
|
|
25
26
|
Invoke sdbtool directly with [`uvx`](https://docs.astral.sh/uv/):
|
|
26
27
|
|
|
27
28
|
```shell
|
|
28
|
-
uvx sdbtool your.sdb # Convert the file 'your.sdb' to xml, and print it to the console
|
|
29
|
-
uvx sdbtool your.sdb --output your.xml # Convert the file 'your.sdb' to xml, and write it to 'your.xml'
|
|
29
|
+
uvx sdbtool sdb2xml your.sdb # Convert the file 'your.sdb' to xml, and print it to the console
|
|
30
|
+
uvx sdbtool sdb2xml your.sdb --output your.xml # Convert the file 'your.sdb' to xml, and write it to 'your.xml'
|
|
31
|
+
uvx sdbtool attributes your.exe # Show the file attributes as recognized by apphelp in an XML-friendly format
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
Or install sdbtool with `uv` (recommended), `pip`, or `pipx`:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sdbtool"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Dump SDB file to xml using apphelp.dll"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -28,7 +28,8 @@ CI = "https://github.com/learn-more/sdbtool/actions"
|
|
|
28
28
|
Repository = "https://github.com/learn-more/sdbtool"
|
|
29
29
|
|
|
30
30
|
[project.scripts]
|
|
31
|
-
sdbtool = "sdbtool.cli:
|
|
31
|
+
sdbtool = "sdbtool.cli:sdbtool_command"
|
|
32
|
+
sdb2xml = "sdbtool.cli:sdb2xml_command"
|
|
32
33
|
|
|
33
34
|
[build-system]
|
|
34
35
|
requires = ["hatchling"]
|
|
File without changes
|
|
@@ -15,9 +15,10 @@ class PathType(IntEnum):
|
|
|
15
15
|
NT_PATH = 1
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
TAG_NULL = 0x0
|
|
18
19
|
TAGID_NULL = 0x0
|
|
19
20
|
TAGID_ROOT = 0x0
|
|
20
|
-
|
|
21
|
+
SHIMDB_INDEX_UNIQUE_KEY = 0x1
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class TagType(IntEnum):
|
|
@@ -35,10 +36,7 @@ class TagType(IntEnum):
|
|
|
35
36
|
MASK = 0xF000
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _get_tag_type(tag: int) -> TagType:
|
|
39
|
+
def get_tag_type(tag: int) -> TagType:
|
|
42
40
|
"""Extracts the type from a tag."""
|
|
43
41
|
return TagType(tag & TagType.MASK)
|
|
44
42
|
|
|
@@ -48,6 +46,19 @@ def tag_to_string(tag: int) -> str:
|
|
|
48
46
|
return apphelp.SdbTagToString(tag)
|
|
49
47
|
|
|
50
48
|
|
|
49
|
+
def guid_to_string(guid: bytes) -> str:
|
|
50
|
+
"""Converts a GUID (16-byte binary) to its string representation."""
|
|
51
|
+
if len(guid) != 16:
|
|
52
|
+
raise ValueError("GUID must be 16 bytes long")
|
|
53
|
+
return (
|
|
54
|
+
f"{guid[3]:02x}{guid[2]:02x}{guid[1]:02x}{guid[0]:02x}-"
|
|
55
|
+
f"{guid[5]:02x}{guid[4]:02x}-"
|
|
56
|
+
f"{guid[7]:02x}{guid[6]:02x}-"
|
|
57
|
+
f"{guid[8]:02x}{guid[9]:02x}-"
|
|
58
|
+
f"{guid[10]:02x}{guid[11]:02x}{guid[12]:02x}{guid[13]:02x}{guid[14]:02x}{guid[15]:02x}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
51
62
|
class Tag:
|
|
52
63
|
def __init__(self, db: "SdbDatabase", tag_id: int):
|
|
53
64
|
self.db = db
|
|
@@ -59,7 +70,7 @@ class Tag:
|
|
|
59
70
|
else:
|
|
60
71
|
self.tag = apphelp.SdbGetTagFromTagID(self._ensure_db_handle(), tag_id)
|
|
61
72
|
self.name = apphelp.SdbTagToString(self.tag)
|
|
62
|
-
self.type =
|
|
73
|
+
self.type = get_tag_type(self.tag)
|
|
63
74
|
|
|
64
75
|
def _ensure_db_handle(self) -> c_void_p:
|
|
65
76
|
"""Ensures that the database handle is initialized."""
|
|
@@ -6,14 +6,19 @@ COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from ctypes import (
|
|
9
|
+
byref,
|
|
10
|
+
create_unicode_buffer,
|
|
9
11
|
windll,
|
|
10
12
|
c_void_p,
|
|
11
13
|
c_uint16,
|
|
12
14
|
c_uint32,
|
|
13
15
|
c_wchar_p,
|
|
14
16
|
POINTER,
|
|
17
|
+
pointer,
|
|
15
18
|
c_ubyte,
|
|
16
19
|
c_uint64,
|
|
20
|
+
Structure,
|
|
21
|
+
Union,
|
|
17
22
|
)
|
|
18
23
|
|
|
19
24
|
APPHELP = windll.apphelp
|
|
@@ -66,6 +71,86 @@ APPHELP.SdbGetStringTagPtr.argtypes = [c_void_p, c_uint32]
|
|
|
66
71
|
APPHELP.SdbGetStringTagPtr.restype = c_wchar_p
|
|
67
72
|
|
|
68
73
|
|
|
74
|
+
# typedef struct tagATTRINFO {
|
|
75
|
+
# TAG tAttrID;
|
|
76
|
+
# DWORD dwFlags;
|
|
77
|
+
# union {
|
|
78
|
+
# ULONGLONG ullAttr;
|
|
79
|
+
# DWORD dwAttr;
|
|
80
|
+
# TCHAR *lpAttr;
|
|
81
|
+
# };
|
|
82
|
+
# } ATTRINFO, *PATTRINFO;
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _U(Union):
|
|
86
|
+
_fields_ = [("ullAttr", c_uint64), ("dwAttr", c_uint32), ("lpAttr", c_wchar_p)]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ATTRINFO(Structure):
|
|
90
|
+
_anonymous_ = ("_u",)
|
|
91
|
+
_fields_ = [
|
|
92
|
+
("tAttrID", c_uint16), # TAG
|
|
93
|
+
("dwFlags", c_uint32),
|
|
94
|
+
("_u", _U), # Union for ullAttr, dwAttr, lpAttr
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
ATTRIBUTE_AVAILABLE = 0x00000001 # Attribute is available
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# BOOL WINAPI SdbFormatAttribute(
|
|
102
|
+
# _In_ PATTRINFO pAttrInfo,
|
|
103
|
+
# _Out_ LPTSTR pchBuffer,
|
|
104
|
+
# _In_ DWORD dwBufferSize
|
|
105
|
+
# );
|
|
106
|
+
APPHELP.SdbFormatAttribute.argtypes = [POINTER(ATTRINFO), c_wchar_p, c_uint32]
|
|
107
|
+
APPHELP.SdbFormatAttribute.restype = c_uint32
|
|
108
|
+
|
|
109
|
+
# BOOL WINAPI SdbGetFileAttributes(
|
|
110
|
+
# _In_ LPCTSTR lpwszFileName,
|
|
111
|
+
# _Out_ PATTRINFO *ppAttrInfo,
|
|
112
|
+
# _Out_ LPDWORD lpdwAttrCount
|
|
113
|
+
# );
|
|
114
|
+
APPHELP.SdbGetFileAttributes.argtypes = [
|
|
115
|
+
c_wchar_p,
|
|
116
|
+
POINTER(POINTER(ATTRINFO)),
|
|
117
|
+
POINTER(c_uint32),
|
|
118
|
+
]
|
|
119
|
+
APPHELP.SdbGetFileAttributes.restype = c_uint32
|
|
120
|
+
|
|
121
|
+
# BOOL WINAPI SdbFreeFileAttributes(
|
|
122
|
+
# _In_ PATTRINFO pFileAttributes
|
|
123
|
+
# );
|
|
124
|
+
APPHELP.SdbFreeFileAttributes.argtypes = [c_void_p]
|
|
125
|
+
APPHELP.SdbFreeFileAttributes.restype = c_uint32
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# BOOL WINAPI SdbGetMatchingExe(
|
|
129
|
+
# _In_opt_ HSDB hSDB,
|
|
130
|
+
# _In_ LPCTSTR szPath,
|
|
131
|
+
# _In_opt_ LPCTSTR szModuleName,
|
|
132
|
+
# _In_opt_ LPCTSTR pszEnvironment,
|
|
133
|
+
# _In_ DWORD dwFlags,
|
|
134
|
+
# _Out_ PSDBQUERYRESULT pQueryResult
|
|
135
|
+
# );
|
|
136
|
+
APPHELP.SdbGetMatchingExe.argtypes = [
|
|
137
|
+
c_void_p,
|
|
138
|
+
c_wchar_p,
|
|
139
|
+
c_wchar_p,
|
|
140
|
+
c_wchar_p,
|
|
141
|
+
c_uint32,
|
|
142
|
+
POINTER(c_void_p),
|
|
143
|
+
]
|
|
144
|
+
APPHELP.SdbGetMatchingExe.restype = c_uint32
|
|
145
|
+
|
|
146
|
+
# void WINAPI SdbReleaseMatchingExe(
|
|
147
|
+
# _In_ HSDB hSDB,
|
|
148
|
+
# _In_ TAGREF trExe
|
|
149
|
+
# );
|
|
150
|
+
APPHELP.SdbReleaseMatchingExe.argtypes = [c_void_p, c_uint32]
|
|
151
|
+
APPHELP.SdbReleaseMatchingExe.restype = None
|
|
152
|
+
|
|
153
|
+
|
|
69
154
|
def SdbOpenDatabase(path: str, path_type: int) -> c_void_p:
|
|
70
155
|
"""Open a database at the specified path."""
|
|
71
156
|
return APPHELP.SdbOpenDatabase(path, path_type)
|
|
@@ -126,3 +211,31 @@ def SdbReadBinaryTag(db: c_void_p, tag_id: int) -> bytes:
|
|
|
126
211
|
def SdbGetStringTagPtr(db: c_void_p, tag_id: int) -> str:
|
|
127
212
|
"""Get the string pointer of the specified tag."""
|
|
128
213
|
return APPHELP.SdbGetStringTagPtr(db, tag_id)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def GetFileAttributes(file_name: str):
|
|
217
|
+
"""Get file attributes for the specified file."""
|
|
218
|
+
attr_info = POINTER(ATTRINFO)()
|
|
219
|
+
attr_count = c_uint32()
|
|
220
|
+
result = APPHELP.SdbGetFileAttributes(
|
|
221
|
+
file_name, byref(attr_info), byref(attr_count)
|
|
222
|
+
)
|
|
223
|
+
if result == 0:
|
|
224
|
+
raise ValueError(f"Failed to get file attributes for '{file_name}'")
|
|
225
|
+
return attr_info, attr_count
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def SdbFormatAttribute(attr_info: ATTRINFO) -> str:
|
|
229
|
+
"""Format the attribute information into a string."""
|
|
230
|
+
buffer_size = 1024 * 2
|
|
231
|
+
buffer = create_unicode_buffer(buffer_size)
|
|
232
|
+
result = APPHELP.SdbFormatAttribute(pointer(attr_info), buffer, buffer_size)
|
|
233
|
+
if result == 0:
|
|
234
|
+
name = SdbTagToString(attr_info.tAttrID)
|
|
235
|
+
raise ValueError(f"Failed to format attribute ({name})")
|
|
236
|
+
return buffer.value if buffer.value else ""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def SdbFreeFileAttributes(attr_info):
|
|
240
|
+
"""Free the file attributes structure."""
|
|
241
|
+
APPHELP.SdbFreeFileAttributes(attr_info)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PROJECT: sdbtool
|
|
3
|
+
LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
4
|
+
PURPOSE: Wrapper around the low level apphelp file attributes API
|
|
5
|
+
COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from sdbtool.apphelp.winapi import (
|
|
9
|
+
GetFileAttributes,
|
|
10
|
+
SdbFormatAttribute,
|
|
11
|
+
SdbFreeFileAttributes,
|
|
12
|
+
ATTRIBUTE_AVAILABLE,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_attributes(file_name: str) -> list[str]:
|
|
17
|
+
attr_info, attr_count = GetFileAttributes(file_name)
|
|
18
|
+
res = [
|
|
19
|
+
SdbFormatAttribute(attr_info[i])
|
|
20
|
+
for i in range(attr_count.value)
|
|
21
|
+
if attr_info[i].dwFlags & ATTRIBUTE_AVAILABLE != 0
|
|
22
|
+
]
|
|
23
|
+
SdbFreeFileAttributes(attr_info)
|
|
24
|
+
return res
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PROJECT: sdbtool
|
|
3
|
+
LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
4
|
+
PURPOSE: Entrypoint of the sdbtool tool
|
|
5
|
+
COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from sdbtool import sdb2xml
|
|
10
|
+
from sdbtool.attributes import get_attributes
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="sdbtool", help="A command-line tool for working with SDB files.")
|
|
15
|
+
@click.version_option()
|
|
16
|
+
def sdbtool_command():
|
|
17
|
+
"""sdbtool: A command-line tool for working with SDB files."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@sdbtool_command.command("sdb2xml")
|
|
22
|
+
@click.argument("input_file", type=click.Path(exists=True, dir_okay=False))
|
|
23
|
+
@click.option(
|
|
24
|
+
"--output",
|
|
25
|
+
type=click.File("w", encoding="utf-8"),
|
|
26
|
+
default="-",
|
|
27
|
+
help="Path to the output XML file, or '-' for stdout.",
|
|
28
|
+
)
|
|
29
|
+
def sdb2xml_command(input_file, output):
|
|
30
|
+
"""Convert an SDB file to XML format."""
|
|
31
|
+
try:
|
|
32
|
+
sdb2xml.convert(input_file, output)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
click.echo(f"Error converting SDB to XML: {e}")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@sdbtool_command.command("attributes")
|
|
39
|
+
@click.argument("files", type=click.Path(exists=True, dir_okay=False), nargs=-1)
|
|
40
|
+
def attributes_command(files):
|
|
41
|
+
"""Display file attributes as recognized by AppHelp."""
|
|
42
|
+
for file_name in files:
|
|
43
|
+
try:
|
|
44
|
+
attrs = get_attributes(file_name)
|
|
45
|
+
except ValueError as e:
|
|
46
|
+
click.echo(f"Error getting attributes for {file_name}: {e}")
|
|
47
|
+
continue
|
|
48
|
+
click.echo(f"Attributes for {file_name}:")
|
|
49
|
+
if not attrs:
|
|
50
|
+
click.echo(" No attributes found.")
|
|
51
|
+
continue
|
|
52
|
+
for attr in attrs:
|
|
53
|
+
click.echo(f" {attr}")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PROJECT: sdbtool
|
|
3
|
+
LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
4
|
+
PURPOSE: Convert SDB files to XML format.
|
|
5
|
+
COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from sdbtool.apphelp import (
|
|
9
|
+
PathType,
|
|
10
|
+
SdbDatabase,
|
|
11
|
+
TagVisitor,
|
|
12
|
+
TagType,
|
|
13
|
+
Tag,
|
|
14
|
+
tag_to_string,
|
|
15
|
+
guid_to_string,
|
|
16
|
+
SHIMDB_INDEX_UNIQUE_KEY
|
|
17
|
+
)
|
|
18
|
+
from sdbtool.xml import XmlWriter
|
|
19
|
+
from base64 import b64encode
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def tagtype_to_xmltype(tag_type: TagType) -> str | None:
|
|
24
|
+
tagtype_map = {
|
|
25
|
+
TagType.BYTE: "xs:byte",
|
|
26
|
+
TagType.WORD: "xs:unsignedShort",
|
|
27
|
+
TagType.DWORD: "xs:unsignedInt",
|
|
28
|
+
TagType.QWORD: "xs:unsignedLong",
|
|
29
|
+
TagType.STRINGREF: "xs:string",
|
|
30
|
+
TagType.STRING: "xs:string",
|
|
31
|
+
TagType.BINARY: "xs:base64Binary",
|
|
32
|
+
}
|
|
33
|
+
return tagtype_map.get(tag_type, None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class XmlTagVisitor(TagVisitor):
|
|
37
|
+
def __init__(self, stream, input_filename: str):
|
|
38
|
+
"""Initialize the XML tag visitor with a filename."""
|
|
39
|
+
self.writer = XmlWriter(stream)
|
|
40
|
+
self._first = True
|
|
41
|
+
self._input_filename = input_filename
|
|
42
|
+
|
|
43
|
+
def visit_list_begin(self, tag: Tag):
|
|
44
|
+
"""Visit the beginning of a list tag."""
|
|
45
|
+
attrs = None
|
|
46
|
+
if self._first:
|
|
47
|
+
self._first = False
|
|
48
|
+
self.writer.write_xml_declaration()
|
|
49
|
+
attrs = {
|
|
50
|
+
"xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
|
51
|
+
"file": self._input_filename,
|
|
52
|
+
}
|
|
53
|
+
self.writer.open(tag.name, attrs)
|
|
54
|
+
|
|
55
|
+
def visit_list_end(self, tag: Tag):
|
|
56
|
+
"""Visit the end of a list tag."""
|
|
57
|
+
self.writer.close(tag.name)
|
|
58
|
+
|
|
59
|
+
def visit(self, tag: Tag):
|
|
60
|
+
"""Visit a tag."""
|
|
61
|
+
if tag.type == TagType.NULL:
|
|
62
|
+
self.writer.empty_tag(tag.name)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
attrs = {}
|
|
66
|
+
if tag.type != TagType.LIST:
|
|
67
|
+
typename = tagtype_to_xmltype(tag.type)
|
|
68
|
+
if typename is not None:
|
|
69
|
+
attrs["type"] = typename
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f"Unknown tag type: {tag.type} for tag {tag.name}")
|
|
72
|
+
|
|
73
|
+
self.writer.open(tag.name, attrs)
|
|
74
|
+
self._write_tag_value(tag)
|
|
75
|
+
self.writer.close(tag.name)
|
|
76
|
+
|
|
77
|
+
def _write_tag_value(self, tag: Tag):
|
|
78
|
+
"""Write the value of a tag based on its type."""
|
|
79
|
+
if tag.type == TagType.BYTE:
|
|
80
|
+
self.writer.write_comment(
|
|
81
|
+
"UNHANDLED BYTE TAG, please report this at https://github.com/learn-more/sdbtool"
|
|
82
|
+
)
|
|
83
|
+
elif tag.type == TagType.WORD:
|
|
84
|
+
value = tag.as_word()
|
|
85
|
+
self.writer.write(f"{value}")
|
|
86
|
+
if tag.name in ("INDEX_TAG", "INDEX_KEY"):
|
|
87
|
+
self.writer.write_comment(f"{tag_to_string(value)}")
|
|
88
|
+
elif tag.type == TagType.DWORD:
|
|
89
|
+
value = tag.as_dword()
|
|
90
|
+
self.writer.write(f"{value}")
|
|
91
|
+
if tag.name in ("INDEX_FLAGS",):
|
|
92
|
+
comment = ""
|
|
93
|
+
if value & SHIMDB_INDEX_UNIQUE_KEY:
|
|
94
|
+
comment += "1 = SHIMDB_INDEX_UNIQUE_KEY" # https://learn.microsoft.com/en-us/windows/win32/devnotes/sdbgetindex
|
|
95
|
+
if comment:
|
|
96
|
+
self.writer.write_comment(comment)
|
|
97
|
+
elif tag.type == TagType.QWORD:
|
|
98
|
+
self.writer.write(f"{tag.as_qword()}")
|
|
99
|
+
elif tag.type in (TagType.STRINGREF, TagType.STRING):
|
|
100
|
+
val = tag.as_string()
|
|
101
|
+
if val:
|
|
102
|
+
self.writer.write(f"{val}")
|
|
103
|
+
elif tag.type == TagType.BINARY:
|
|
104
|
+
data = tag.as_bytes()
|
|
105
|
+
if data:
|
|
106
|
+
base64_data = b64encode(data).decode("utf-8")
|
|
107
|
+
self.writer.write(base64_data)
|
|
108
|
+
if tag.name.endswith("_ID") and len(data) == 16:
|
|
109
|
+
guid_str = guid_to_string(data)
|
|
110
|
+
self.writer.write_comment(f"{{{guid_str}}}")
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"Unknown tag type: {tag.type} for tag {tag.name}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def convert(input_file: str, output_stream):
|
|
116
|
+
with SdbDatabase(input_file, PathType.DOS_PATH) as db:
|
|
117
|
+
if not db:
|
|
118
|
+
raise ValueError(f"Failed to open database at '{input_file}'")
|
|
119
|
+
|
|
120
|
+
visitor = XmlTagVisitor(output_stream, Path(input_file).name)
|
|
121
|
+
root = db.root()
|
|
122
|
+
if root is None:
|
|
123
|
+
raise ValueError(f"No root tag found in database '{input_file}'")
|
|
124
|
+
root.accept(visitor)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PROJECT: sdbtool
|
|
3
|
+
LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
4
|
+
PURPOSE: Simple streaming Xml writer
|
|
5
|
+
COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from xml.sax.saxutils import escape, quoteattr
|
|
9
|
+
|
|
10
|
+
INDENT_DEPTH = 2 # Number of spaces for each indentation level in XML output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class XmlWriter:
|
|
14
|
+
def __init__(self, stream):
|
|
15
|
+
self._stream = stream
|
|
16
|
+
self._indent_level = 0
|
|
17
|
+
self._indent_on_close = False
|
|
18
|
+
|
|
19
|
+
def write_xml_declaration(self):
|
|
20
|
+
"""Write the XML declaration at the start of the document."""
|
|
21
|
+
self._stream.write('<?xml version="1.0" encoding="utf-8" standalone="yes"?>')
|
|
22
|
+
|
|
23
|
+
def _indent(self):
|
|
24
|
+
"""Return the indentation string for the given level."""
|
|
25
|
+
self._stream.write("\n")
|
|
26
|
+
self._stream.write(" " * (self._indent_level * INDENT_DEPTH))
|
|
27
|
+
|
|
28
|
+
def open(self, name, attrib=None):
|
|
29
|
+
"""Open an XML tag with the given name and attributes."""
|
|
30
|
+
self._indent()
|
|
31
|
+
self._stream.write(f"<{name}")
|
|
32
|
+
if attrib:
|
|
33
|
+
for key, value in attrib.items():
|
|
34
|
+
self._stream.write(f" {key}={quoteattr(value)}")
|
|
35
|
+
self._stream.write(">")
|
|
36
|
+
|
|
37
|
+
self._indent_level += 1
|
|
38
|
+
self._indent_on_close = False
|
|
39
|
+
|
|
40
|
+
def close(self, name):
|
|
41
|
+
"""Close an XML tag with the given name."""
|
|
42
|
+
self._indent_level -= 1
|
|
43
|
+
if self._indent_on_close:
|
|
44
|
+
self._indent()
|
|
45
|
+
self._stream.write(f"</{name}>")
|
|
46
|
+
self._indent_on_close = True
|
|
47
|
+
|
|
48
|
+
def empty_tag(self, name):
|
|
49
|
+
"""Write an empty XML tag with the given name and attributes."""
|
|
50
|
+
self._indent()
|
|
51
|
+
self._stream.write(f"<{name} />")
|
|
52
|
+
self._indent_on_close = True
|
|
53
|
+
|
|
54
|
+
def write(self, text):
|
|
55
|
+
"""Write text content to the XML stream."""
|
|
56
|
+
self._stream.write(escape(text))
|
|
57
|
+
|
|
58
|
+
def write_comment(self, comment):
|
|
59
|
+
"""Write a comment to the XML stream."""
|
|
60
|
+
self._stream.write(f"<!-- {escape(comment)} -->")
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PROJECT: sdbtool
|
|
3
|
-
LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
4
|
-
PURPOSE: Entrypoint of the sdbtool tool, which converts SDB files to XML format.
|
|
5
|
-
COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import sdbtool.apphelp as apphelp
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from sdbtool.xml import XmlTagVisitor
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def sdb2xml(input_file: str, output_stream):
|
|
15
|
-
with apphelp.SdbDatabase(input_file, apphelp.PathType.DOS_PATH) as db:
|
|
16
|
-
if not db:
|
|
17
|
-
print(f"Failed to open database at '{input_file}'")
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
visitor = XmlTagVisitor(output_stream, Path(input_file).name)
|
|
21
|
-
root = db.root()
|
|
22
|
-
if root is None:
|
|
23
|
-
print(f"No root tag found in database '{input_file}'")
|
|
24
|
-
return
|
|
25
|
-
root.accept(visitor)
|
sdbtool-0.3.2/sdbtool/cli.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from sdbtool import sdb2xml
|
|
2
|
-
import click
|
|
3
|
-
|
|
4
|
-
@click.command()
|
|
5
|
-
@click.version_option()
|
|
6
|
-
@click.argument("input_file", type=click.Path(exists=True, dir_okay=False))
|
|
7
|
-
@click.option(
|
|
8
|
-
"--output",
|
|
9
|
-
type=click.File("w", encoding="utf-8"),
|
|
10
|
-
default="-",
|
|
11
|
-
help="Path to the output XML file, or '-' for stdout.",
|
|
12
|
-
)
|
|
13
|
-
def cli(input_file, output):
|
|
14
|
-
sdb2xml(input_file, output)
|
sdbtool-0.3.2/sdbtool/xml.py
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PROJECT: sdbtool
|
|
3
|
-
LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
4
|
-
PURPOSE: Xml writer + visitor for SDB files.
|
|
5
|
-
COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from sdbtool.apphelp import TagVisitor, Tag, TagType, tag_to_string
|
|
9
|
-
from xml.sax.saxutils import escape, quoteattr
|
|
10
|
-
from base64 import b64encode
|
|
11
|
-
|
|
12
|
-
INDENT_DEPTH = 2 # Number of spaces for each indentation level in XML output
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def tagtype_to_xmltype(tag_type: TagType) -> str | None:
|
|
16
|
-
switch = {
|
|
17
|
-
TagType.BYTE: "xs:byte",
|
|
18
|
-
TagType.WORD: "xs:unsignedShort",
|
|
19
|
-
TagType.DWORD: "xs:unsignedInt",
|
|
20
|
-
TagType.QWORD: "xs:unsignedLong",
|
|
21
|
-
TagType.STRINGREF: "xs:string",
|
|
22
|
-
TagType.STRING: "xs:string",
|
|
23
|
-
TagType.BINARY: "xs:base64Binary",
|
|
24
|
-
}
|
|
25
|
-
return switch.get(tag_type, None)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class XmlWriter:
|
|
29
|
-
def __init__(self, stream):
|
|
30
|
-
self._stream = stream
|
|
31
|
-
|
|
32
|
-
def write_xml_declaration(self):
|
|
33
|
-
"""Write the XML declaration at the start of the document."""
|
|
34
|
-
self._stream.write('<?xml version="1.0" encoding="utf-8" standalone="yes"?>')
|
|
35
|
-
|
|
36
|
-
def indent(self, level):
|
|
37
|
-
"""Return the indentation string for the given level."""
|
|
38
|
-
self._stream.write("\n")
|
|
39
|
-
self._stream.write(" " * (level * INDENT_DEPTH))
|
|
40
|
-
return self
|
|
41
|
-
|
|
42
|
-
def open(self, name, attrib=None):
|
|
43
|
-
"""Open an XML tag with the given name and attributes."""
|
|
44
|
-
self._stream.write(f"<{name}")
|
|
45
|
-
if attrib:
|
|
46
|
-
for key, value in attrib.items():
|
|
47
|
-
self._stream.write(f' {key}={quoteattr(value)}')
|
|
48
|
-
self._stream.write(">")
|
|
49
|
-
return self
|
|
50
|
-
|
|
51
|
-
def close(self, name):
|
|
52
|
-
"""Close an XML tag with the given name."""
|
|
53
|
-
self._stream.write(f"</{name}>")
|
|
54
|
-
|
|
55
|
-
def empty_tag(self, name):
|
|
56
|
-
"""Write an empty XML tag with the given name and attributes."""
|
|
57
|
-
self._stream.write(f"<{name} />")
|
|
58
|
-
|
|
59
|
-
def write(self, text):
|
|
60
|
-
"""Write text content to the XML stream."""
|
|
61
|
-
self._stream.write(text)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class XmlTagVisitor(TagVisitor):
|
|
65
|
-
def __init__(self, stream, input_filename: str):
|
|
66
|
-
"""Initialize the XML tag visitor with a filename."""
|
|
67
|
-
self.writer = XmlWriter(stream)
|
|
68
|
-
self.writer.write_xml_declaration()
|
|
69
|
-
self._depth = 0
|
|
70
|
-
self._input_filename = input_filename
|
|
71
|
-
self._indent_on_close = False
|
|
72
|
-
|
|
73
|
-
def visit_list_begin(self, tag: Tag):
|
|
74
|
-
"""Visit the beginning of a list tag."""
|
|
75
|
-
|
|
76
|
-
attrs = None
|
|
77
|
-
if self._depth == 0:
|
|
78
|
-
attrs = {
|
|
79
|
-
"xmlns:xs": "http://www.w3.org/2001/XMLSchema",
|
|
80
|
-
"file": self._input_filename,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
self.writer.indent(self._depth).open(tag.name, attrs)
|
|
84
|
-
self._depth += 1
|
|
85
|
-
self._indent_on_close = False
|
|
86
|
-
|
|
87
|
-
def visit_list_end(self, tag: Tag):
|
|
88
|
-
"""Visit the end of a list tag."""
|
|
89
|
-
self._depth -= 1
|
|
90
|
-
if self._indent_on_close:
|
|
91
|
-
self.writer.indent(self._depth)
|
|
92
|
-
self.writer.close(tag.name)
|
|
93
|
-
self._indent_on_close = True
|
|
94
|
-
|
|
95
|
-
def visit(self, tag: Tag):
|
|
96
|
-
"""Visit a tag."""
|
|
97
|
-
self._indent_on_close = True
|
|
98
|
-
if tag.type == TagType.NULL:
|
|
99
|
-
self.writer.indent(self._depth).empty_tag(tag.name)
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
attrs = {}
|
|
103
|
-
if tag.type != TagType.LIST:
|
|
104
|
-
typename = tagtype_to_xmltype(tag.type)
|
|
105
|
-
if typename is not None:
|
|
106
|
-
attrs["type"] = typename
|
|
107
|
-
else:
|
|
108
|
-
raise ValueError(f"Unknown tag type: {tag.type} for tag {tag.name}")
|
|
109
|
-
|
|
110
|
-
self.writer.indent(self._depth).open(tag.name, attrs)
|
|
111
|
-
self._write_tag_value(tag)
|
|
112
|
-
self.writer.close(tag.name)
|
|
113
|
-
|
|
114
|
-
def _write_tag_value(self, tag: Tag):
|
|
115
|
-
"""Write the value of a tag based on its type."""
|
|
116
|
-
if tag.type == TagType.BYTE:
|
|
117
|
-
self.writer.write(
|
|
118
|
-
"<!-- UNHANDLED BYTE TAG, please report this at https://github.com/learn-more/sdbtool -->"
|
|
119
|
-
)
|
|
120
|
-
elif tag.type == TagType.WORD:
|
|
121
|
-
value = tag.as_word()
|
|
122
|
-
self.writer.write(f"{value}")
|
|
123
|
-
if tag.name in ("INDEX_TAG", "INDEX_KEY"):
|
|
124
|
-
self.writer.write(f"<!-- {tag_to_string(value)} -->")
|
|
125
|
-
elif tag.type == TagType.DWORD:
|
|
126
|
-
self.writer.write(f"{tag.as_dword()}")
|
|
127
|
-
elif tag.type == TagType.QWORD:
|
|
128
|
-
self.writer.write(f"{tag.as_qword()}")
|
|
129
|
-
elif tag.type in (TagType.STRINGREF, TagType.STRING):
|
|
130
|
-
val = tag.as_string()
|
|
131
|
-
if val:
|
|
132
|
-
self.writer.write(f"{escape(val)}")
|
|
133
|
-
elif tag.type == TagType.BINARY:
|
|
134
|
-
data = tag.as_bytes()
|
|
135
|
-
if data:
|
|
136
|
-
base64_data = b64encode(data).decode("utf-8")
|
|
137
|
-
self.writer.write(base64_data)
|
|
138
|
-
else:
|
|
139
|
-
raise ValueError(f"Unknown tag type: {tag.type} for tag {tag.name}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|