sdbtool 0.3.0__tar.gz → 0.3.2__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.
@@ -0,0 +1,54 @@
1
+ name: Publish Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ release-build:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install the latest version of uv and set the python version
18
+ uses: astral-sh/setup-uv@v6
19
+ with:
20
+ python-version: "3.10"
21
+
22
+ - name: Build release distributions
23
+ run: |
24
+ uv build
25
+
26
+ - name: Upload distributions
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: release-dists
30
+ path: dist/
31
+
32
+ pypi-publish:
33
+ runs-on: ubuntu-latest
34
+ needs:
35
+ - release-build
36
+ permissions:
37
+ id-token: write
38
+
39
+ # Dedicated environments with protections for publishing are strongly recommended.
40
+ # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
41
+ environment:
42
+ name: pypi
43
+
44
+ steps:
45
+ - name: Retrieve release distributions
46
+ uses: actions/download-artifact@v4
47
+ with:
48
+ name: release-dists
49
+ path: dist/
50
+
51
+ - name: Publish release distributions to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+ with:
54
+ packages-dir: dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdbtool
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdbtool"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Dump SDB file to xml using apphelp.dll"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1,25 @@
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)
@@ -0,0 +1,162 @@
1
+ """
2
+ PROJECT: sdbtool
3
+ LICENSE: MIT (https://spdx.org/licenses/MIT)
4
+ PURPOSE: High level interface to the AppHelp API for reading SDB files.
5
+ COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
6
+ """
7
+
8
+ from ctypes import c_void_p
9
+ from enum import IntEnum
10
+ import sdbtool.apphelp.winapi as apphelp
11
+
12
+
13
+ class PathType(IntEnum):
14
+ DOS_PATH = 0
15
+ NT_PATH = 1
16
+
17
+
18
+ TAGID_NULL = 0x0
19
+ TAGID_ROOT = 0x0
20
+ _TAGID_ROOT = 12
21
+
22
+
23
+ class TagType(IntEnum):
24
+ """Enumeration of tag types."""
25
+
26
+ NULL = 0x1000
27
+ BYTE = 0x2000
28
+ WORD = 0x3000
29
+ DWORD = 0x4000
30
+ QWORD = 0x5000
31
+ STRINGREF = 0x6000
32
+ LIST = 0x7000
33
+ STRING = 0x8000
34
+ BINARY = 0x9000
35
+ MASK = 0xF000
36
+
37
+
38
+ TAG_NULL = 0x0
39
+
40
+
41
+ def _get_tag_type(tag: int) -> TagType:
42
+ """Extracts the type from a tag."""
43
+ return TagType(tag & TagType.MASK)
44
+
45
+
46
+ def tag_to_string(tag: int) -> str:
47
+ """Converts a tag to its string representation."""
48
+ return apphelp.SdbTagToString(tag)
49
+
50
+
51
+ class Tag:
52
+ def __init__(self, db: "SdbDatabase", tag_id: int):
53
+ self.db = db
54
+ self.tag_id = tag_id
55
+ if tag_id == TAGID_ROOT:
56
+ self.tag = TAG_NULL
57
+ self.name = "SDB"
58
+ self.type = TagType.LIST
59
+ else:
60
+ self.tag = apphelp.SdbGetTagFromTagID(self._ensure_db_handle(), tag_id)
61
+ self.name = apphelp.SdbTagToString(self.tag)
62
+ self.type = _get_tag_type(self.tag)
63
+
64
+ def _ensure_db_handle(self) -> c_void_p:
65
+ """Ensures that the database handle is initialized."""
66
+ if self.db._handle is None:
67
+ raise ValueError("Database handle is not initialized")
68
+ return self.db._handle
69
+
70
+ def tags(self):
71
+ self._ensure_db_handle()
72
+ child = apphelp.SdbGetFirstChild(self._ensure_db_handle(), self.tag_id)
73
+ while child != 0:
74
+ yield Tag(self.db, child)
75
+ child = apphelp.SdbGetNextChild(
76
+ self._ensure_db_handle(), self.tag_id, child
77
+ )
78
+
79
+ def as_word(self, default: int = 0) -> int:
80
+ """Returns the tag value as a word (16-bit integer)."""
81
+ if self.type != TagType.WORD:
82
+ raise ValueError(f"Tag {self.name} is not a WORD type")
83
+ return apphelp.SdbReadWORDTag(self._ensure_db_handle(), self.tag_id, default)
84
+
85
+ def as_dword(self, default: int = 0) -> int:
86
+ """Returns the tag value as a dword (32-bit integer)."""
87
+ if self.type != TagType.DWORD:
88
+ raise ValueError(f"Tag {self.name} is not a DWORD type")
89
+ return apphelp.SdbReadDWORDTag(self._ensure_db_handle(), self.tag_id, default)
90
+
91
+ def as_qword(self, default: int = 0) -> int:
92
+ """Returns the tag value as a qword (64-bit integer)."""
93
+ if self.type != TagType.QWORD:
94
+ raise ValueError(f"Tag {self.name} is not a QWORD type")
95
+ return apphelp.SdbReadQWORDTag(self._ensure_db_handle(), self.tag_id, default)
96
+
97
+ def as_bytes(self) -> bytes:
98
+ """Returns the tag value as bytes."""
99
+ if self.type != TagType.BINARY:
100
+ raise ValueError(f"Tag {self.name} is not a BINARY type")
101
+ return apphelp.SdbReadBinaryTag(self._ensure_db_handle(), self.tag_id)
102
+
103
+ def as_string(self) -> str:
104
+ """Returns the tag value as a string."""
105
+ if self.type not in (TagType.STRING, TagType.STRINGREF):
106
+ raise ValueError(f"Tag {self.name} is not a STRING or STRINGREF type")
107
+ ptr = apphelp.SdbGetStringTagPtr(self._ensure_db_handle(), self.tag_id)
108
+ return ptr if ptr is not None else ""
109
+
110
+ def accept(self, visitor: "TagVisitor"):
111
+ """Accepts a visitor for this tag."""
112
+ if self.type == TagType.LIST:
113
+ visitor.visit_list_begin(self)
114
+ for child in self.tags():
115
+ child.accept(visitor)
116
+ visitor.visit_list_end(self)
117
+ else:
118
+ # For non-list tags, we just visit this tag
119
+ visitor.visit(self)
120
+
121
+
122
+ class TagVisitor:
123
+ def visit(self, tag: Tag):
124
+ """Visit a tag. Override this method in subclasses."""
125
+ raise NotImplementedError("Subclasses must implement visit method")
126
+
127
+ def visit_list_begin(self, tag: Tag):
128
+ """Visit a list tag. Override this method in subclasses."""
129
+ raise NotImplementedError("Subclasses must implement visit_list_begin method")
130
+
131
+ def visit_list_end(self, tag: Tag):
132
+ """Visit the end of a list tag. Override this method in subclasses."""
133
+ raise NotImplementedError("Subclasses must implement visit_list_end method")
134
+
135
+
136
+ class SdbDatabase:
137
+ def __init__(self, path: str, path_type: PathType):
138
+ self.path = path
139
+ self.path_type = path_type
140
+ self._handle = apphelp.SdbOpenDatabase(path, path_type)
141
+ self._root = None
142
+
143
+ def root(self) -> Tag | None:
144
+ if self._root is None and self._handle is not None:
145
+ self._root = Tag(self, TAGID_ROOT)
146
+ return self._root
147
+
148
+ def close(self):
149
+ if self._handle:
150
+ apphelp.SdbCloseDatabase(self._handle)
151
+ self._handle = None
152
+
153
+ def __bool__(self):
154
+ if self._handle is None:
155
+ return False
156
+ return True
157
+
158
+ def __enter__(self):
159
+ return self
160
+
161
+ def __exit__(self, exc_type, exc_value, traceback):
162
+ self.close()
@@ -0,0 +1,128 @@
1
+ """
2
+ PROJECT: sdbtool
3
+ LICENSE: MIT (https://spdx.org/licenses/MIT)
4
+ PURPOSE: winapi interface to the AppHelp API for reading SDB files.
5
+ COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
6
+ """
7
+
8
+ from ctypes import (
9
+ windll,
10
+ c_void_p,
11
+ c_uint16,
12
+ c_uint32,
13
+ c_wchar_p,
14
+ POINTER,
15
+ c_ubyte,
16
+ c_uint64,
17
+ )
18
+
19
+ APPHELP = windll.apphelp
20
+
21
+ # PDB WINAPI SdbOpenDatabase(LPCWSTR path, PATH_TYPE type);
22
+ APPHELP.SdbOpenDatabase.argtypes = [c_wchar_p, c_uint32]
23
+ APPHELP.SdbOpenDatabase.restype = c_void_p
24
+
25
+ # void WINAPI SdbCloseDatabase(PDB);
26
+ APPHELP.SdbCloseDatabase.argtypes = [c_void_p]
27
+
28
+ # TAGID WINAPI SdbGetFirstChild(PDB pdb, TAGID parent);
29
+ APPHELP.SdbGetFirstChild.argtypes = [c_void_p, c_uint32]
30
+ APPHELP.SdbGetFirstChild.restype = c_uint32
31
+
32
+ # TAGID WINAPI SdbGetNextChild(PDB pdb, TAGID parent, TAGID prev_child);
33
+ APPHELP.SdbGetNextChild.argtypes = [c_void_p, c_uint32, c_uint32]
34
+ APPHELP.SdbGetNextChild.restype = c_uint32
35
+
36
+ # TAG WINAPI SdbGetTagFromTagID(PDB pdb, TAGID tagid);
37
+ APPHELP.SdbGetTagFromTagID.argtypes = [c_void_p, c_uint32]
38
+ APPHELP.SdbGetTagFromTagID.restype = c_uint16
39
+
40
+ # LPCWSTR WINAPI SdbTagToString(TAG tag);
41
+ APPHELP.SdbTagToString.argtypes = [c_uint16]
42
+ APPHELP.SdbTagToString.restype = c_wchar_p
43
+
44
+ # WORD WINAPI SdbReadDWORDTag(PDB pdb, TAGID tagid, WORD ret);
45
+ APPHELP.SdbReadWORDTag.argtypes = [c_void_p, c_uint32, c_uint16]
46
+ APPHELP.SdbReadWORDTag.restype = c_uint16
47
+
48
+ # DWORD WINAPI SdbReadDWORDTag(PDB pdb, TAGID tagid, DWORD ret);
49
+ APPHELP.SdbReadDWORDTag.argtypes = [c_void_p, c_uint32, c_uint32]
50
+ APPHELP.SdbReadDWORDTag.restype = c_uint32
51
+
52
+ # QWORD WINAPI SdbReadQWORDTag(PDB pdb, TAGID tagid, QWORD ret);
53
+ APPHELP.SdbReadQWORDTag.argtypes = [c_void_p, c_uint32, c_uint64]
54
+ APPHELP.SdbReadQWORDTag.restype = c_uint64
55
+
56
+ # DWORD WINAPI SdbGetTagDataSize(PDB pdb, TAGID tagid);
57
+ APPHELP.SdbGetTagDataSize.argtypes = [c_void_p, c_uint32]
58
+ APPHELP.SdbGetTagDataSize.restype = c_uint32
59
+
60
+ # BOOL WINAPI SdbReadBinaryTag(PDB pdb, TAGID tagid, PBYTE buffer, DWORD size);
61
+ APPHELP.SdbReadBinaryTag.argtypes = [c_void_p, c_uint32, POINTER(c_ubyte), c_uint32]
62
+ APPHELP.SdbReadBinaryTag.restype = c_uint32
63
+
64
+ # LPWSTR WINAPI SdbGetStringTagPtr(PDB pdb, TAGID tagid);
65
+ APPHELP.SdbGetStringTagPtr.argtypes = [c_void_p, c_uint32]
66
+ APPHELP.SdbGetStringTagPtr.restype = c_wchar_p
67
+
68
+
69
+ def SdbOpenDatabase(path: str, path_type: int) -> c_void_p:
70
+ """Open a database at the specified path."""
71
+ return APPHELP.SdbOpenDatabase(path, path_type)
72
+
73
+
74
+ def SdbCloseDatabase(db: c_void_p):
75
+ """Close the specified database."""
76
+ APPHELP.SdbCloseDatabase(db)
77
+
78
+
79
+ def SdbGetFirstChild(db: c_void_p, parent: int) -> int:
80
+ """Get the first child tag of the specified parent."""
81
+ return APPHELP.SdbGetFirstChild(db, parent)
82
+
83
+
84
+ def SdbGetNextChild(db: c_void_p, parent: int, prev_child: int) -> int:
85
+ """Get the next child tag of the specified parent."""
86
+ return APPHELP.SdbGetNextChild(db, parent, prev_child)
87
+
88
+
89
+ def SdbGetTagFromTagID(db: c_void_p, tag_id: int) -> int:
90
+ """Get the tag from the specified tag ID."""
91
+ return APPHELP.SdbGetTagFromTagID(db, tag_id)
92
+
93
+
94
+ def SdbTagToString(tag: int) -> str:
95
+ """Convert a tag to its string representation."""
96
+ return APPHELP.SdbTagToString(tag)
97
+
98
+
99
+ def SdbReadWORDTag(db: c_void_p, tag_id: int, default: int = 0) -> int:
100
+ """Read a WORD tag from the database."""
101
+ return APPHELP.SdbReadWORDTag(db, tag_id, default)
102
+
103
+
104
+ def SdbReadDWORDTag(db: c_void_p, tag_id: int, default: int = 0) -> int:
105
+ """Read a DWORD tag from the database."""
106
+ return APPHELP.SdbReadDWORDTag(db, tag_id, default)
107
+
108
+
109
+ def SdbReadQWORDTag(db: c_void_p, tag_id: int, default: int = 0) -> int:
110
+ """Read a QWORD tag from the database."""
111
+ return APPHELP.SdbReadQWORDTag(db, tag_id, default)
112
+
113
+
114
+ def SdbReadBinaryTag(db: c_void_p, tag_id: int) -> bytes:
115
+ """Read a binary tag from the database."""
116
+ size = APPHELP.SdbGetTagDataSize(db, tag_id)
117
+ if size == 0:
118
+ return b""
119
+ data = (c_ubyte * size)()
120
+ result = APPHELP.SdbReadBinaryTag(db, tag_id, data, size)
121
+ if result == 0:
122
+ raise ValueError(f"Failed to read binary tag 0x{tag_id:x}")
123
+ return bytes(data)
124
+
125
+
126
+ def SdbGetStringTagPtr(db: c_void_p, tag_id: int) -> str:
127
+ """Get the string pointer of the specified tag."""
128
+ return APPHELP.SdbGetStringTagPtr(db, tag_id)
@@ -1,4 +1,4 @@
1
- from sdbtool import convert
1
+ from sdbtool import sdb2xml
2
2
  import click
3
3
 
4
4
  @click.command()
@@ -11,4 +11,4 @@ import click
11
11
  help="Path to the output XML file, or '-' for stdout.",
12
12
  )
13
13
  def cli(input_file, output):
14
- convert(input_file, output)
14
+ sdb2xml(input_file, output)
@@ -0,0 +1,139 @@
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}")
@@ -1,131 +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
- import sdbtool.apphelp as apphelp
8
- from base64 import b64encode
9
- from xml.sax.saxutils import escape
10
-
11
- INDENT_DEPTH = 2
12
-
13
- def tagtype_to_xmltype(tag_type: int) -> str|None:
14
- switch = {
15
- apphelp.TAG_TYPE_BYTE: "xs:byte",
16
- apphelp.TAG_TYPE_WORD: "xs:unsignedShort",
17
- apphelp.TAG_TYPE_DWORD: "xs:unsignedInt",
18
- apphelp.TAG_TYPE_QWORD: "xs:unsignedLong",
19
- apphelp.TAG_TYPE_STRINGREF: "xs:string",
20
- apphelp.TAG_TYPE_STRING: "xs:string",
21
- apphelp.TAG_TYPE_BINARY: "xs:base64Binary",
22
- }
23
- return switch.get(tag_type, None)
24
-
25
- class Xml:
26
- def __init__(self, stream, name, indent, attrib=None, xmltag=False):
27
- self._stream = stream
28
- self.name = name
29
- self.indent = indent
30
- self.attrib = attrib or {}
31
- self._has_children = False
32
- self._xmltag = xmltag
33
- self._delay = None
34
- self._closed = False
35
-
36
- def node(self, name, attrib=None):
37
- self._has_children = True
38
- return Xml(self._stream, name, self.indent + 1, attrib)
39
-
40
- def flush(self):
41
- if self._delay:
42
- self._stream.write(self._delay)
43
- self._delay = None
44
-
45
- def write(self, text):
46
- self.flush()
47
- self._stream.write(text)
48
-
49
- def close(self):
50
- assert self._delay is not None, "Xml.close() called when tag has content"
51
- self._stream.write(" />")
52
- self._delay = None
53
- self._closed = True
54
-
55
- def __enter__(self):
56
- if self._xmltag:
57
- self.write('<?xml version="1.0" encoding="utf-8" standalone="yes"?>')
58
- self._xmltag = False
59
- self.write("\n")
60
- if self.indent:
61
- self.write(" " * (self.indent*INDENT_DEPTH))
62
- self.write(f"<{self.name}")
63
- for key, value in self.attrib.items():
64
- self.write(f' {key}="{value}"')
65
-
66
- self._delay = ">"
67
- return self
68
-
69
- def __exit__(self, exc_type, exc_value, traceback):
70
- if self._closed:
71
- return
72
- if self._has_children:
73
- self.write("\n")
74
- if self.indent:
75
- self.write(" " * (self.indent*INDENT_DEPTH))
76
- self.write(f"</{self.name}>")
77
-
78
-
79
- def dump_tag(node, tag):
80
- if tag.type == apphelp.TAG_TYPE_LIST:
81
- node.flush()
82
- for childtag in tag.tags():
83
- attrs = {}
84
- if childtag.type != apphelp.TAG_TYPE_LIST and childtag.type != apphelp.TAG_TYPE_NULL:
85
- typename = tagtype_to_xmltype(childtag.type)
86
- assert typename is not None, f"Unknown tag type: {childtag.tag}"
87
- attrs["type"] = typename
88
- with node.node(childtag.name, attrs) as childnode:
89
- dump_tag(childnode, childtag)
90
- return
91
-
92
- assert next(tag.tags(), None) is None, f"Tag {tag.name} is not a list but has children"
93
-
94
- if tag.type == apphelp.TAG_TYPE_NULL:
95
- node.close()
96
- elif tag.type == apphelp.TAG_TYPE_BYTE:
97
- node.write("<!-- UNHANDLED BYTE TAG, please report this at https://github.com/learn-more/sdbtool -->")
98
- elif tag.type == apphelp.TAG_TYPE_WORD:
99
- val = tag.as_word()
100
- node.write(f"{val}")
101
- if node.name in ("INDEX_TAG", "INDEX_KEY"):
102
- node.write(f"<!-- {apphelp.tag_to_string(val)} -->")
103
- elif tag.type == apphelp.TAG_TYPE_DWORD:
104
- node.write(f"{tag.as_dword()}")
105
- elif tag.type == apphelp.TAG_TYPE_QWORD:
106
- node.write(f"{tag.as_qword()}")
107
- elif tag.type in (apphelp.TAG_TYPE_STRINGREF, apphelp.TAG_TYPE_STRING):
108
- val = escape(tag.as_string())
109
- node.write(f"{val}")
110
- elif tag.type == apphelp.TAG_TYPE_BINARY:
111
- data = tag.as_bytes()
112
- if not data:
113
- node.close()
114
- else:
115
- base64_data = b64encode(data).decode('utf-8')
116
- node.write(base64_data)
117
- else:
118
- raise ValueError(f"Unknown tag type: {tag.type} for tag {tag.name}")
119
-
120
-
121
- def convert(input_file: str, output_stream):
122
- with apphelp.SdbDatabase(input_file, apphelp.PathType.DOS_PATH) as db:
123
- if not db:
124
- print(f"Failed to open database at '{input_file}'")
125
- return
126
- attrs = {
127
- "xmlns:xs": "http://www.w3.org/2001/XMLSchema",
128
- "path": input_file,
129
- }
130
- with Xml(output_stream, "SDB", 0, attrs, xmltag=True) as node:
131
- dump_tag(node, db.root())
@@ -1,174 +0,0 @@
1
- '''
2
- PROJECT: sdbtool
3
- LICENSE: MIT (https://spdx.org/licenses/MIT)
4
- PURPOSE: interface to the AppHelp API for reading SDB files.
5
- COPYRIGHT: Copyright 2025 Mark Jansen <mark.jansen@reactos.org>
6
- '''
7
- from ctypes import windll, c_void_p, c_uint16, c_uint32, c_wchar_p, POINTER, c_ubyte, c_uint64
8
- from enum import IntEnum
9
-
10
- class PathType(IntEnum):
11
- DOS_PATH = 0
12
- NT_PATH = 1
13
-
14
-
15
- TAGID_NULL = 0x0
16
- TAGID_ROOT = 0x0
17
- _TAGID_ROOT = 12
18
-
19
- TAG_TYPE_MASK = 0xF000
20
-
21
- TAG_TYPE_NULL = 0x1000
22
- TAG_TYPE_BYTE = 0x2000
23
- TAG_TYPE_WORD = 0x3000
24
- TAG_TYPE_DWORD = 0x4000
25
- TAG_TYPE_QWORD = 0x5000
26
- TAG_TYPE_STRINGREF = 0x6000
27
- TAG_TYPE_LIST = 0x7000
28
- TAG_TYPE_STRING = 0x8000
29
- TAG_TYPE_BINARY = 0x9000
30
- TAG_NULL = 0x0
31
-
32
-
33
- APPHELP = windll.apphelp
34
-
35
- # PDB WINAPI SdbOpenDatabase(LPCWSTR path, PATH_TYPE type);
36
- APPHELP.SdbOpenDatabase.argtypes = [c_wchar_p, c_uint32]
37
- APPHELP.SdbOpenDatabase.restype = c_void_p
38
-
39
- # void WINAPI SdbCloseDatabase(PDB);
40
- APPHELP.SdbCloseDatabase.argtypes = [c_void_p]
41
-
42
- # TAGID WINAPI SdbGetFirstChild(PDB pdb, TAGID parent);
43
- APPHELP.SdbGetFirstChild.argtypes = [c_void_p, c_uint32]
44
- APPHELP.SdbGetFirstChild.restype = c_uint32
45
-
46
- # TAGID WINAPI SdbGetNextChild(PDB pdb, TAGID parent, TAGID prev_child);
47
- APPHELP.SdbGetNextChild.argtypes = [c_void_p, c_uint32, c_uint32]
48
- APPHELP.SdbGetNextChild.restype = c_uint32
49
-
50
- # TAG WINAPI SdbGetTagFromTagID(PDB pdb, TAGID tagid);
51
- APPHELP.SdbGetTagFromTagID.argtypes = [c_void_p, c_uint32]
52
- APPHELP.SdbGetTagFromTagID.restype = c_uint16
53
-
54
- # LPCWSTR WINAPI SdbTagToString(TAG tag);
55
- APPHELP.SdbTagToString.argtypes = [c_uint16]
56
- APPHELP.SdbTagToString.restype = c_wchar_p
57
-
58
- # WORD WINAPI SdbReadDWORDTag(PDB pdb, TAGID tagid, WORD ret);
59
- APPHELP.SdbReadWORDTag.argtypes = [c_void_p, c_uint32, c_uint16]
60
- APPHELP.SdbReadWORDTag.restype = c_uint16
61
-
62
- # DWORD WINAPI SdbReadDWORDTag(PDB pdb, TAGID tagid, DWORD ret);
63
- APPHELP.SdbReadDWORDTag.argtypes = [c_void_p, c_uint32, c_uint32]
64
- APPHELP.SdbReadDWORDTag.restype = c_uint32
65
-
66
- # QWORD WINAPI SdbReadQWORDTag(PDB pdb, TAGID tagid, QWORD ret);
67
- APPHELP.SdbReadQWORDTag.argtypes = [c_void_p, c_uint32, c_uint64]
68
- APPHELP.SdbReadQWORDTag.restype = c_uint64
69
-
70
- # DWORD WINAPI SdbGetTagDataSize(PDB pdb, TAGID tagid);
71
- APPHELP.SdbGetTagDataSize.argtypes = [c_void_p, c_uint32]
72
- APPHELP.SdbGetTagDataSize.restype = c_uint32
73
-
74
- # BOOL WINAPI SdbReadBinaryTag(PDB pdb, TAGID tagid, PBYTE buffer, DWORD size);
75
- APPHELP.SdbReadBinaryTag.argtypes = [c_void_p, c_uint32, POINTER(c_ubyte), c_uint32]
76
- APPHELP.SdbReadBinaryTag.restype = c_uint32
77
-
78
- # LPWSTR WINAPI SdbGetStringTagPtr(PDB pdb, TAGID tagid);
79
- APPHELP.SdbGetStringTagPtr.argtypes = [c_void_p, c_uint32]
80
- APPHELP.SdbGetStringTagPtr.restype = c_wchar_p
81
-
82
- def _get_tag_type(tag: int) -> int:
83
- """Extracts the type from a tag."""
84
- return tag & TAG_TYPE_MASK
85
-
86
- def tag_to_string(tag: int) -> str:
87
- """Converts a tag to its string representation."""
88
- return APPHELP.SdbTagToString(tag)
89
-
90
- class Tag:
91
- def __init__(self, db: 'SdbDatabase', tag_id: int):
92
- self.db = db
93
- self.tag_id = tag_id
94
- if tag_id == TAGID_ROOT:
95
- self.tag = TAG_NULL
96
- self.name = 'SDB'
97
- self.type = TAG_TYPE_LIST
98
- else:
99
- self.tag = APPHELP.SdbGetTagFromTagID(db.handle, tag_id)
100
- self.name = APPHELP.SdbTagToString(self.tag)
101
- self.type = _get_tag_type(self.tag)
102
-
103
- def tags(self):
104
- child = APPHELP.SdbGetFirstChild(self.db.handle, self.tag_id)
105
- while child != 0:
106
- yield Tag(self.db, child)
107
- child = APPHELP.SdbGetNextChild(self.db.handle, self.tag_id, child)
108
-
109
- def as_word(self, default: int = 0) -> int:
110
- """Returns the tag value as a word (16-bit integer)."""
111
- if self.type != TAG_TYPE_WORD:
112
- raise ValueError(f"Tag {self.name} is not a WORD type")
113
- return APPHELP.SdbReadWORDTag(self.db.handle, self.tag_id, default)
114
-
115
- def as_dword(self, default: int = 0) -> int:
116
- """Returns the tag value as a dword (32-bit integer)."""
117
- if self.type != TAG_TYPE_DWORD:
118
- raise ValueError(f"Tag {self.name} is not a DWORD type")
119
- return APPHELP.SdbReadDWORDTag(self.db.handle, self.tag_id, default)
120
-
121
- def as_qword(self, default: int = 0) -> int:
122
- """Returns the tag value as a qword (64-bit integer)."""
123
- if self.type != TAG_TYPE_QWORD:
124
- raise ValueError(f"Tag {self.name} is not a QWORD type")
125
- return APPHELP.SdbReadQWORDTag(self.db.handle, self.tag_id, default)
126
-
127
- def as_bytes(self) -> bytes:
128
- """Returns the tag value as bytes."""
129
- if self.type != TAG_TYPE_BINARY:
130
- raise ValueError(f"Tag {self.name} is not a BINARY type")
131
- size = APPHELP.SdbGetTagDataSize(self.db.handle, self.tag_id)
132
- if size == 0:
133
- return b''
134
- data = (c_ubyte * size)()
135
- result = APPHELP.SdbReadBinaryTag(self.db.handle, self.tag_id, data, size)
136
- if result == 0:
137
- raise ValueError(f"Failed to read binary tag {self.name}")
138
- return bytes(data)
139
-
140
- def as_string(self) -> str:
141
- """Returns the tag value as a string."""
142
- if self.type not in (TAG_TYPE_STRING, TAG_TYPE_STRINGREF):
143
- raise ValueError(f"Tag {self.name} is not a STRING or STRINGREF type")
144
- ptr = APPHELP.SdbGetStringTagPtr(self.db.handle, self.tag_id)
145
- return ptr if ptr is not None else ''
146
-
147
-
148
- class SdbDatabase:
149
- def __init__(self, path: str, path_type: PathType):
150
- self.path = path
151
- self.path_type = path_type
152
- self.handle = APPHELP.SdbOpenDatabase(path, path_type)
153
- self._root = None
154
-
155
- def root(self):
156
- if self._root is None and self.handle is not None:
157
- self._root = Tag(self, TAGID_ROOT)
158
- return self._root
159
-
160
- def close(self):
161
- if self.handle:
162
- APPHELP.SdbCloseDatabase(self.handle)
163
- self.handle = None
164
-
165
- def __bool__(self):
166
- if self.handle is None:
167
- return False
168
- return True
169
-
170
- def __enter__(self):
171
- return self
172
-
173
- def __exit__(self, exc_type, exc_value, traceback):
174
- self.close()
File without changes
File without changes
File without changes
File without changes