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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdbtool
3
- Version: 0.3.2
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.2"
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: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
@@ -0,0 +1,4 @@
1
+ from sdbtool.cli import sdbtool_command
2
+
3
+ if __name__ == "__main__":
4
+ sdbtool_command()
@@ -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
- _TAGID_ROOT = 12
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
- TAG_NULL = 0x0
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 = _get_tag_type(self.tag)
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)
@@ -1,4 +0,0 @@
1
- from sdbtool.cli import cli
2
-
3
- if __name__ == "__main__":
4
- cli()
@@ -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)
@@ -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