qres 0.1.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.
qres-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Farhan Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
qres-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: qres
3
+ Version: 0.1.0
4
+ Summary: Fast Qt resource compiler — Python C extension
5
+ Author-email: Farhan Ali <i.farhanali.dev@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Farhan Ali
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/farhaanaliii/qres
29
+ Project-URL: Repository, https://github.com/farhaanaliii/qres
30
+ Project-URL: Issues, https://github.com/farhaanaliii/qres/issues
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Programming Language :: C
37
+ Classifier: Topic :: Software Development :: Build Tools
38
+ Classifier: Operating System :: OS Independent
39
+ Requires-Python: >=3.11
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Dynamic: license-file
43
+
44
+ <div align="center">
45
+
46
+ # qres
47
+
48
+ [![PyPI](https://img.shields.io/pypi/v/qres)](https://pypi.org/project/qres)
49
+ [![Python](https://img.shields.io/pypi/pyversions/qres)](https://pypi.org/project/qres)
50
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
51
+ [![Build](https://img.shields.io/github/actions/workflow/status/farhaanaliii/qres/ci.yml?branch=main)](https://github.com/farhaanaliii/qres/actions)
52
+
53
+ A fast Qt resource compiler for Python, implemented as a C extension. Drop-in replacement for `pyrcc5` / `pyside6-rcc` that compiles `.qrc` files into `_rc.py` modules without requiring a Qt installation.
54
+
55
+ </div>
56
+
57
+ ## Installation
58
+
59
+ ```
60
+ pip install qres
61
+ ```
62
+
63
+ Pre-built wheels are available for Linux (x86_64, aarch64), macOS (x86_64, arm64), and Windows (AMD64, ARM64) on Python 3.11+.
64
+
65
+ ## Usage
66
+
67
+ **CLI**
68
+
69
+ ```
70
+ qres resources.qrc
71
+ qres resources.qrc -o path/to/resources_rc.py
72
+ qres resources.qrc --binding PySide6
73
+ ```
74
+
75
+ **Python API**
76
+
77
+ ```python
78
+ import qres
79
+
80
+ # Compile from a file path
81
+ qres.compile_file("resources.qrc")
82
+ qres.compile_file("resources.qrc", output="resources_rc.py")
83
+ qres.compile_file("resources.qrc", binding="PySide6")
84
+
85
+ # Compile from an XML string, returns raw blobs
86
+ blobs = qres.compile(xml_string, base_dir="/path/to/qrc/dir")
87
+ # blobs: {"data": bytes, "name": bytes, "struct_v1": bytes, "struct_v2": bytes}
88
+ ```
89
+
90
+ The generated `_rc.py` works with `qtpy`, `PyQt5`, `PyQt6`, `PySide2`, and `PySide6`. By default, it includes an automatic fallback import chain that resolves whichever Qt binding is installed in the active environment.
91
+
92
+ ## How it works
93
+
94
+ qres parses the `.qrc` XML, builds a virtual file tree sorted by Qt's hash function, compresses each asset with zlib at level 9 (skipping compression when it doesn't help or for `.ico` files), then serializes two binary struct formats — `v1` for Qt < 5.8 and `v2` for Qt >= 5.8 — into Python byte literals.
95
+
96
+ ## Benchmarks
97
+
98
+ Benchmarked on a 50 MB uncompressed text asset compiled into a registered Qt resource module:
99
+
100
+ | Metric | Measurement |
101
+ |---|---|
102
+ | Input payload | 50.00 MB (52,428,800 bytes) |
103
+ | Compile duration | 403 ms (0.40 s) |
104
+ | Compilation throughput | 124 MB/s |
105
+ | Generated module size | 1.23 MB (97.5% compression) |
106
+ | Qt resource read speed | 93 ms (534 MB/s) |
107
+ | Data integrity | SHA-256 verified |
108
+
109
+
110
+ ## Contributing
111
+
112
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md).
113
+
114
+ ## Building from source
115
+
116
+ Requires a C compiler.
117
+
118
+ ```
119
+ git clone https://github.com/farhaanaliii/qres
120
+ cd qres
121
+ pip install -e .
122
+ ```
123
+
124
+ ## License
125
+
126
+ MIT — see [LICENSE](LICENSE).
qres-0.1.0/README.md ADDED
@@ -0,0 +1,83 @@
1
+ <div align="center">
2
+
3
+ # qres
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/qres)](https://pypi.org/project/qres)
6
+ [![Python](https://img.shields.io/pypi/pyversions/qres)](https://pypi.org/project/qres)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ [![Build](https://img.shields.io/github/actions/workflow/status/farhaanaliii/qres/ci.yml?branch=main)](https://github.com/farhaanaliii/qres/actions)
9
+
10
+ A fast Qt resource compiler for Python, implemented as a C extension. Drop-in replacement for `pyrcc5` / `pyside6-rcc` that compiles `.qrc` files into `_rc.py` modules without requiring a Qt installation.
11
+
12
+ </div>
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ pip install qres
18
+ ```
19
+
20
+ Pre-built wheels are available for Linux (x86_64, aarch64), macOS (x86_64, arm64), and Windows (AMD64, ARM64) on Python 3.11+.
21
+
22
+ ## Usage
23
+
24
+ **CLI**
25
+
26
+ ```
27
+ qres resources.qrc
28
+ qres resources.qrc -o path/to/resources_rc.py
29
+ qres resources.qrc --binding PySide6
30
+ ```
31
+
32
+ **Python API**
33
+
34
+ ```python
35
+ import qres
36
+
37
+ # Compile from a file path
38
+ qres.compile_file("resources.qrc")
39
+ qres.compile_file("resources.qrc", output="resources_rc.py")
40
+ qres.compile_file("resources.qrc", binding="PySide6")
41
+
42
+ # Compile from an XML string, returns raw blobs
43
+ blobs = qres.compile(xml_string, base_dir="/path/to/qrc/dir")
44
+ # blobs: {"data": bytes, "name": bytes, "struct_v1": bytes, "struct_v2": bytes}
45
+ ```
46
+
47
+ The generated `_rc.py` works with `qtpy`, `PyQt5`, `PyQt6`, `PySide2`, and `PySide6`. By default, it includes an automatic fallback import chain that resolves whichever Qt binding is installed in the active environment.
48
+
49
+ ## How it works
50
+
51
+ qres parses the `.qrc` XML, builds a virtual file tree sorted by Qt's hash function, compresses each asset with zlib at level 9 (skipping compression when it doesn't help or for `.ico` files), then serializes two binary struct formats — `v1` for Qt < 5.8 and `v2` for Qt >= 5.8 — into Python byte literals.
52
+
53
+ ## Benchmarks
54
+
55
+ Benchmarked on a 50 MB uncompressed text asset compiled into a registered Qt resource module:
56
+
57
+ | Metric | Measurement |
58
+ |---|---|
59
+ | Input payload | 50.00 MB (52,428,800 bytes) |
60
+ | Compile duration | 403 ms (0.40 s) |
61
+ | Compilation throughput | 124 MB/s |
62
+ | Generated module size | 1.23 MB (97.5% compression) |
63
+ | Qt resource read speed | 93 ms (534 MB/s) |
64
+ | Data integrity | SHA-256 verified |
65
+
66
+
67
+ ## Contributing
68
+
69
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md).
70
+
71
+ ## Building from source
72
+
73
+ Requires a C compiler.
74
+
75
+ ```
76
+ git clone https://github.com/farhaanaliii/qres
77
+ cd qres
78
+ pip install -e .
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qres"
7
+ version = "0.1.0"
8
+ description = "Fast Qt resource compiler — Python C extension"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [{ name = "Farhan Ali", email = "i.farhanali.dev@gmail.com" }]
12
+ requires-python = ">=3.11"
13
+ dependencies = []
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: C",
21
+ "Topic :: Software Development :: Build Tools",
22
+ "Operating System :: OS Independent",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/farhaanaliii/qres"
27
+ Repository = "https://github.com/farhaanaliii/qres"
28
+ Issues = "https://github.com/farhaanaliii/qres/issues"
29
+
30
+ [project.scripts]
31
+ qres = "qres:main"
32
+
33
+ [tool.setuptools]
34
+ packages = ["qres"]
35
+
36
+ [tool.setuptools.package-data]
37
+ qres = ["py.typed"]
38
+
@@ -0,0 +1,110 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ import sys
4
+
5
+ from ._qres import compile as _compile
6
+
7
+ _BINDINGS = ("PyQt6", "PySide6", "PyQt5", "PySide2", "qtpy")
8
+
9
+ _HEADER_BOILERPLATE = """# -*- coding: utf-8 -*-
10
+
11
+ # Resource object code
12
+ #
13
+ # Created by: qres
14
+ #
15
+ # WARNING! All changes made in this file will be lost!
16
+
17
+ """
18
+
19
+ _FOOTER_BOILERPLATE = """qt_version = [int(v) for v in QtCore.qVersion().split('.')]
20
+ if qt_version < [5, 8, 0]:
21
+ rcc_version = 1
22
+ qt_resource_struct = qt_resource_struct_v1
23
+ else:
24
+ rcc_version = 2
25
+ qt_resource_struct = qt_resource_struct_v2
26
+
27
+ def qInitResources():
28
+ QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
29
+
30
+ def qCleanupResources():
31
+ QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
32
+
33
+ qInitResources()
34
+ """
35
+
36
+ _RESOURCE_BLOBS = (
37
+ ("qt_resource_data", "data"),
38
+ ("qt_resource_name", "name"),
39
+ ("qt_resource_struct_v1", "struct_v1"),
40
+ ("qt_resource_struct_v2", "struct_v2"),
41
+ )
42
+
43
+ _BYTE_ESCAPES = tuple(f"\\x{byte:02x}" for byte in range(256))
44
+
45
+
46
+ def compile(xml: str, base_dir: str) -> dict[str, bytes]:
47
+ return _compile(xml, base_dir)
48
+
49
+ def _generate_import_header(binding: str) -> str:
50
+ if binding != "auto":
51
+ return f"from {binding} import QtCore\n\n"
52
+
53
+ lines = []
54
+ for i, name in enumerate(_BINDINGS[:-1]):
55
+ indent = " " * i
56
+ lines.append(f"{indent}try:\n{indent} from {name} import QtCore\n{indent}except ImportError:\n")
57
+
58
+ indent = " " * (len(_BINDINGS) - 1)
59
+ lines.append(f"{indent}from {_BINDINGS[-1]} import QtCore\n\n")
60
+ return "".join(lines)
61
+
62
+ def _generate_resource_blob(name: str, data: bytes) -> str:
63
+ return (
64
+ f'{name} = b"\\\n'
65
+ + "".join(
66
+ "".join(_BYTE_ESCAPES[byte] for byte in data[i:i + 16]) + "\\\n"
67
+ for i in range(0, len(data), 16)
68
+ )
69
+ + '"\n\n'
70
+ )
71
+
72
+
73
+ def compile_file(
74
+ qrc_path: str | Path,
75
+ output: str | Path | None = None,
76
+ binding: str = "auto",
77
+ ) -> None:
78
+ qrc_path = Path(qrc_path)
79
+ blobs = compile(qrc_path.read_text(encoding="utf-8"), str(qrc_path.parent))
80
+
81
+ target = Path(output) if output else qrc_path.with_name(f"{qrc_path.stem}_rc.py")
82
+ import_header = _generate_import_header(binding)
83
+ blobs_code = "".join(_generate_resource_blob(var_name, blobs[key]) for var_name, key in _RESOURCE_BLOBS)
84
+
85
+ target.write_text(_HEADER_BOILERPLATE + import_header + blobs_code + _FOOTER_BOILERPLATE, encoding="utf-8")
86
+
87
+
88
+ def main() -> None:
89
+ parser = argparse.ArgumentParser(
90
+ prog="qres",
91
+ description="Compile a .qrc file into a Python resource module.",
92
+ )
93
+ parser.add_argument("qrc", help="Path to the .qrc file")
94
+ parser.add_argument("-o", "--output", default=None, help="Output .py file path")
95
+ parser.add_argument(
96
+ "--binding",
97
+ choices=("auto", *_BINDINGS),
98
+ default="auto",
99
+ help="Target Qt binding (default: auto)",
100
+ )
101
+ parser.add_argument("-v", "--verbose", action="store_true", help="Print verbose output")
102
+ args = parser.parse_args()
103
+
104
+ try:
105
+ compile_file(args.qrc, args.output, binding=args.binding)
106
+ if args.verbose:
107
+ print(f"Resource compilation completed successfully: {args.qrc}")
108
+ except Exception as exc:
109
+ sys.stderr.write(f"qres: error: {exc}\n")
110
+ sys.exit(1)
@@ -0,0 +1,4 @@
1
+ from . import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: qres
3
+ Version: 0.1.0
4
+ Summary: Fast Qt resource compiler — Python C extension
5
+ Author-email: Farhan Ali <i.farhanali.dev@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Farhan Ali
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/farhaanaliii/qres
29
+ Project-URL: Repository, https://github.com/farhaanaliii/qres
30
+ Project-URL: Issues, https://github.com/farhaanaliii/qres/issues
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Programming Language :: C
37
+ Classifier: Topic :: Software Development :: Build Tools
38
+ Classifier: Operating System :: OS Independent
39
+ Requires-Python: >=3.11
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Dynamic: license-file
43
+
44
+ <div align="center">
45
+
46
+ # qres
47
+
48
+ [![PyPI](https://img.shields.io/pypi/v/qres)](https://pypi.org/project/qres)
49
+ [![Python](https://img.shields.io/pypi/pyversions/qres)](https://pypi.org/project/qres)
50
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
51
+ [![Build](https://img.shields.io/github/actions/workflow/status/farhaanaliii/qres/ci.yml?branch=main)](https://github.com/farhaanaliii/qres/actions)
52
+
53
+ A fast Qt resource compiler for Python, implemented as a C extension. Drop-in replacement for `pyrcc5` / `pyside6-rcc` that compiles `.qrc` files into `_rc.py` modules without requiring a Qt installation.
54
+
55
+ </div>
56
+
57
+ ## Installation
58
+
59
+ ```
60
+ pip install qres
61
+ ```
62
+
63
+ Pre-built wheels are available for Linux (x86_64, aarch64), macOS (x86_64, arm64), and Windows (AMD64, ARM64) on Python 3.11+.
64
+
65
+ ## Usage
66
+
67
+ **CLI**
68
+
69
+ ```
70
+ qres resources.qrc
71
+ qres resources.qrc -o path/to/resources_rc.py
72
+ qres resources.qrc --binding PySide6
73
+ ```
74
+
75
+ **Python API**
76
+
77
+ ```python
78
+ import qres
79
+
80
+ # Compile from a file path
81
+ qres.compile_file("resources.qrc")
82
+ qres.compile_file("resources.qrc", output="resources_rc.py")
83
+ qres.compile_file("resources.qrc", binding="PySide6")
84
+
85
+ # Compile from an XML string, returns raw blobs
86
+ blobs = qres.compile(xml_string, base_dir="/path/to/qrc/dir")
87
+ # blobs: {"data": bytes, "name": bytes, "struct_v1": bytes, "struct_v2": bytes}
88
+ ```
89
+
90
+ The generated `_rc.py` works with `qtpy`, `PyQt5`, `PyQt6`, `PySide2`, and `PySide6`. By default, it includes an automatic fallback import chain that resolves whichever Qt binding is installed in the active environment.
91
+
92
+ ## How it works
93
+
94
+ qres parses the `.qrc` XML, builds a virtual file tree sorted by Qt's hash function, compresses each asset with zlib at level 9 (skipping compression when it doesn't help or for `.ico` files), then serializes two binary struct formats — `v1` for Qt < 5.8 and `v2` for Qt >= 5.8 — into Python byte literals.
95
+
96
+ ## Benchmarks
97
+
98
+ Benchmarked on a 50 MB uncompressed text asset compiled into a registered Qt resource module:
99
+
100
+ | Metric | Measurement |
101
+ |---|---|
102
+ | Input payload | 50.00 MB (52,428,800 bytes) |
103
+ | Compile duration | 403 ms (0.40 s) |
104
+ | Compilation throughput | 124 MB/s |
105
+ | Generated module size | 1.23 MB (97.5% compression) |
106
+ | Qt resource read speed | 93 ms (534 MB/s) |
107
+ | Data integrity | SHA-256 verified |
108
+
109
+
110
+ ## Contributing
111
+
112
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md).
113
+
114
+ ## Building from source
115
+
116
+ Requires a C compiler.
117
+
118
+ ```
119
+ git clone https://github.com/farhaanaliii/qres
120
+ cd qres
121
+ pip install -e .
122
+ ```
123
+
124
+ ## License
125
+
126
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ qres/__init__.py
6
+ qres/__main__.py
7
+ qres/py.typed
8
+ qres.egg-info/PKG-INFO
9
+ qres.egg-info/SOURCES.txt
10
+ qres.egg-info/dependency_links.txt
11
+ qres.egg-info/entry_points.txt
12
+ qres.egg-info/top_level.txt
13
+ src/compiler.c
14
+ src/qres.c
15
+ tests/test_compile.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qres = qres:main
@@ -0,0 +1 @@
1
+ qres
qres-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
qres-0.1.0/setup.py ADDED
@@ -0,0 +1,48 @@
1
+ from setuptools import Extension, setup
2
+ from setuptools.command.build_ext import build_ext
3
+
4
+
5
+ class BuildExt(build_ext):
6
+ def build_extensions(self):
7
+ compiler_type = self.compiler.compiler_type
8
+ for ext in self.extensions:
9
+ if compiler_type == "msvc":
10
+ ext.extra_compile_args = [
11
+ "/O2",
12
+ "/W4",
13
+ "/std:c11",
14
+ "/permissive-",
15
+ "/GS",
16
+ "/sdl",
17
+ ]
18
+ else:
19
+ ext.extra_compile_args = [
20
+ "-std=c11",
21
+ "-O2",
22
+ "-Wall",
23
+ "-Wextra",
24
+ "-Wpedantic",
25
+ "-Wshadow",
26
+ "-Wstrict-prototypes",
27
+ "-Wpointer-arith",
28
+ "-Wformat=2",
29
+ "-D_FORTIFY_SOURCE=2",
30
+ "-fstack-protector-strong",
31
+ ]
32
+ super().build_extensions()
33
+
34
+
35
+ setup(
36
+ cmdclass={"build_ext": BuildExt},
37
+ ext_modules=[
38
+ Extension(
39
+ name="qres._qres",
40
+ sources=[
41
+ "src/qres.c",
42
+ "src/compiler.c",
43
+ ],
44
+ include_dirs=["src"],
45
+ )
46
+ ],
47
+ )
48
+
@@ -0,0 +1,541 @@
1
+ #define PY_SSIZE_T_CLEAN
2
+ #include <Python.h>
3
+
4
+ #include "compiler.h"
5
+
6
+ #include <stdlib.h>
7
+ #include <string.h>
8
+
9
+ unsigned int qt_hash(const char *str, unsigned int chained) {
10
+ unsigned int h = chained;
11
+ for (const char *p = str; *p; p++) {
12
+ h = (h << 4) + (unsigned char)(*p);
13
+ unsigned int g = h & 0xF0000000;
14
+ if (g) h ^= g >> 23;
15
+ h &= 0x0FFFFFFF;
16
+ }
17
+ return h;
18
+ }
19
+
20
+ void bb_init(ByteBuffer *bb) {
21
+ bb->data = NULL;
22
+ bb->size = 0;
23
+ bb->capacity = 0;
24
+ }
25
+
26
+ void bb_free(ByteBuffer *bb) {
27
+ free(bb->data);
28
+ bb->data = NULL;
29
+ bb->size = 0;
30
+ bb->capacity = 0;
31
+ }
32
+
33
+ void bb_append(ByteBuffer *bb, const unsigned char *src, size_t len) {
34
+ if (bb->size + len > bb->capacity) {
35
+ size_t new_cap = bb->capacity == 0 ? 1024 : bb->capacity * 2;
36
+ while (bb->size + len > new_cap) new_cap *= 2;
37
+ unsigned char *new_data = (unsigned char *)realloc((void *)bb->data, new_cap);
38
+ if (!new_data) return;
39
+ bb->data = new_data;
40
+ bb->capacity = new_cap;
41
+ }
42
+ memcpy(bb->data + bb->size, src, len);
43
+ bb->size += len;
44
+ }
45
+
46
+ void bb_append_u16be(ByteBuffer *bb, unsigned short val) {
47
+ unsigned char buf[2] = {(unsigned char)((val >> 8) & 0xFF), (unsigned char)(val & 0xFF)};
48
+ bb_append(bb, buf, 2);
49
+ }
50
+
51
+ void bb_append_u32be(ByteBuffer *bb, unsigned int val) {
52
+ unsigned char buf[4] = {
53
+ (unsigned char)((val >> 24) & 0xFF), (unsigned char)((val >> 16) & 0xFF),
54
+ (unsigned char)((val >> 8) & 0xFF), (unsigned char)(val & 0xFF)
55
+ };
56
+ bb_append(bb, buf, 4);
57
+ }
58
+
59
+ void bb_append_i32be(ByteBuffer *bb, int val) {
60
+ unsigned int u = (unsigned int)val;
61
+ unsigned char buf[4] = {
62
+ (unsigned char)((u >> 24) & 0xFF), (unsigned char)((u >> 16) & 0xFF),
63
+ (unsigned char)((u >> 8) & 0xFF), (unsigned char)(u & 0xFF)
64
+ };
65
+ bb_append(bb, buf, 4);
66
+ }
67
+
68
+ void bb_append_u64be(ByteBuffer *bb, unsigned long long val) {
69
+ unsigned char buf[8] = {
70
+ (unsigned char)((val >> 56) & 0xFF), (unsigned char)((val >> 48) & 0xFF),
71
+ (unsigned char)((val >> 40) & 0xFF), (unsigned char)((val >> 32) & 0xFF),
72
+ (unsigned char)((val >> 24) & 0xFF), (unsigned char)((val >> 16) & 0xFF),
73
+ (unsigned char)((val >> 8) & 0xFF), (unsigned char)(val & 0xFF)
74
+ };
75
+ bb_append(bb, buf, 8);
76
+ }
77
+
78
+ ResourceNode *create_node(const char *name, int is_dir, PyObject *data_bytes) {
79
+ ResourceNode *node = (ResourceNode *)calloc(1, sizeof(ResourceNode));
80
+ if (!node) return NULL;
81
+ node->name = strdup(name);
82
+ node->is_dir = is_dir;
83
+ if (data_bytes) {
84
+ node->data_bytes = data_bytes;
85
+ Py_INCREF(data_bytes);
86
+ }
87
+ return node;
88
+ }
89
+
90
+ void free_node(ResourceNode *node) {
91
+ if (!node) return;
92
+ for (int i = 0; i < node->child_count; i++) {
93
+ free_node(node->children[i]);
94
+ }
95
+ free((void *)node->children);
96
+ free(node->name);
97
+ Py_XDECREF(node->data_bytes);
98
+ free(node);
99
+ }
100
+
101
+ static ResourceNode *find_child(ResourceNode *parent, const char *name, int is_dir) {
102
+ for (int i = 0; i < parent->child_count; i++) {
103
+ if (parent->children[i]->is_dir == is_dir &&
104
+ strcmp(parent->children[i]->name, name) == 0) {
105
+ return parent->children[i];
106
+ }
107
+ }
108
+ return NULL;
109
+ }
110
+
111
+ static void add_child(ResourceNode *parent, ResourceNode *child) {
112
+ if (parent->child_count >= parent->child_capacity) {
113
+ int new_cap = parent->child_capacity == 0 ? 4 : parent->child_capacity * 2;
114
+ ResourceNode **new_children = (ResourceNode **)realloc((void *)parent->children, sizeof(ResourceNode *) * (size_t)new_cap);
115
+ if (!new_children) return;
116
+ parent->children = new_children;
117
+ parent->child_capacity = new_cap;
118
+ }
119
+ parent->children[parent->child_count++] = child;
120
+ }
121
+
122
+ static void add_to_tree(ResourceNode *root, const char *virtual_path, PyObject *data_bytes, long long lastmod) {
123
+ char *temp = strdup(virtual_path);
124
+ if (!temp) return;
125
+
126
+ char *p = temp;
127
+ while (*p == '/') p++;
128
+
129
+ char *tok = strtok(p, "/");
130
+ if (!tok) {
131
+ free(temp);
132
+ return;
133
+ }
134
+
135
+ int capacity = 8;
136
+ int part_count = 0;
137
+ char **parts = (char **)malloc(sizeof(char *) * (size_t)capacity);
138
+ if (!parts) {
139
+ free(temp);
140
+ return;
141
+ }
142
+
143
+ while (tok) {
144
+ if (part_count >= capacity) {
145
+ capacity *= 2;
146
+ char **new_parts = (char **)realloc(parts, sizeof(char *) * (size_t)capacity);
147
+ if (!new_parts) {
148
+ free(parts);
149
+ free(temp);
150
+ return;
151
+ }
152
+ parts = new_parts;
153
+ }
154
+ parts[part_count++] = tok;
155
+ tok = strtok(NULL, "/");
156
+ }
157
+
158
+ ResourceNode *current = root;
159
+ for (int i = 0; i < part_count - 1; i++) {
160
+ ResourceNode *child = find_child(current, parts[i], 1);
161
+ if (!child) {
162
+ child = create_node(parts[i], 1, NULL);
163
+ if (!child) {
164
+ free(parts);
165
+ free(temp);
166
+ return;
167
+ }
168
+ add_child(current, child);
169
+ }
170
+ current = child;
171
+ }
172
+
173
+ ResourceNode *file_node = create_node(parts[part_count - 1], 0, data_bytes);
174
+ if (file_node) {
175
+ file_node->lastmod = lastmod;
176
+ add_child(current, file_node);
177
+ }
178
+
179
+ free(parts);
180
+ free(temp);
181
+ }
182
+
183
+ static int parse_qrc(const char *xml_content, ResourceNode *root, const char *base_dir, char *err_buf, size_t err_size) {
184
+ PyObject *etree = PyImport_ImportModule("xml.etree.ElementTree");
185
+ if (!etree) {
186
+ snprintf(err_buf, err_size, "Failed to import xml.etree.ElementTree");
187
+ return -1;
188
+ }
189
+
190
+ PyObject *root_elem = PyObject_CallMethod(etree, "fromstring", "s", xml_content);
191
+ if (!root_elem) {
192
+ PyObject *ptype, *pvalue, *ptrace;
193
+ PyErr_Fetch(&ptype, &pvalue, &ptrace);
194
+ if (pvalue) {
195
+ PyObject *pstr = PyObject_Str(pvalue);
196
+ if (pstr) {
197
+ snprintf(err_buf, err_size, "XML syntax error: %s", PyUnicode_AsUTF8(pstr));
198
+ Py_DECREF(pstr);
199
+ }
200
+ Py_DECREF(pvalue);
201
+ }
202
+ Py_XDECREF(ptype);
203
+ Py_XDECREF(ptrace);
204
+ Py_DECREF(etree);
205
+ return -1;
206
+ }
207
+
208
+ PyObject *pathlib = PyImport_ImportModule("pathlib");
209
+ if (!pathlib) {
210
+ Py_DECREF(root_elem);
211
+ Py_DECREF(etree);
212
+ snprintf(err_buf, err_size, "Failed to import pathlib");
213
+ return -1;
214
+ }
215
+
216
+ PyObject *path_cls = PyObject_GetAttrString(pathlib, "Path");
217
+ PyObject *base_path_obj = PyObject_CallFunction(path_cls, "s", base_dir);
218
+ Py_DECREF(path_cls);
219
+
220
+ PyObject *qresources = PyObject_CallMethod(root_elem, "findall", "s", "qresource");
221
+ if (!qresources) {
222
+ Py_DECREF(base_path_obj);
223
+ Py_DECREF(pathlib);
224
+ Py_DECREF(root_elem);
225
+ Py_DECREF(etree);
226
+ return -1;
227
+ }
228
+
229
+ Py_ssize_t qres_count = PyList_Size(qresources);
230
+ for (Py_ssize_t i = 0; i < qres_count; i++) {
231
+ PyObject *qres = PyList_GetItem(qresources, i);
232
+ PyObject *prefix_obj = PyObject_CallMethod(qres, "get", "ss", "prefix", "");
233
+ const char *raw_prefix = prefix_obj ? PyUnicode_AsUTF8(prefix_obj) : "";
234
+ while (*raw_prefix == '/') raw_prefix++;
235
+
236
+ char clean_prefix[256];
237
+ strncpy(clean_prefix, raw_prefix, sizeof(clean_prefix) - 1);
238
+ clean_prefix[sizeof(clean_prefix) - 1] = '\0';
239
+ size_t plen = strlen(clean_prefix);
240
+ while (plen > 0 && clean_prefix[plen - 1] == '/') {
241
+ clean_prefix[--plen] = '\0';
242
+ }
243
+
244
+ PyObject *files = PyObject_CallMethod(qres, "findall", "s", "file");
245
+ if (!files) {
246
+ Py_XDECREF(prefix_obj);
247
+ continue;
248
+ }
249
+
250
+ Py_ssize_t file_count = PyList_Size(files);
251
+ for (Py_ssize_t j = 0; j < file_count; j++) {
252
+ PyObject *file_item = PyList_GetItem(files, j);
253
+ PyObject *text_obj = PyObject_GetAttrString(file_item, "text");
254
+ if (!text_obj || text_obj == Py_None) {
255
+ Py_XDECREF(text_obj);
256
+ continue;
257
+ }
258
+
259
+ PyObject *stripped_text = PyObject_CallMethod(text_obj, "strip", NULL);
260
+ Py_DECREF(text_obj);
261
+ if (!stripped_text) continue;
262
+
263
+ const char *rel_path = PyUnicode_AsUTF8(stripped_text);
264
+ if (!rel_path || strlen(rel_path) == 0) {
265
+ Py_DECREF(stripped_text);
266
+ continue;
267
+ }
268
+
269
+ PyObject *file_path_obj = PyObject_CallMethod(base_path_obj, "joinpath", "s", rel_path);
270
+ PyObject *data_bytes = PyObject_CallMethod(file_path_obj, "read_bytes", NULL);
271
+ if (!data_bytes) {
272
+ PyErr_Clear();
273
+ PyObject *path_str = PyObject_Str(file_path_obj);
274
+ snprintf(err_buf, err_size, "Cannot open file: %s", path_str ? PyUnicode_AsUTF8(path_str) : rel_path);
275
+ Py_XDECREF(path_str);
276
+ Py_DECREF(file_path_obj);
277
+ Py_DECREF(stripped_text);
278
+ Py_DECREF(files);
279
+ Py_XDECREF(prefix_obj);
280
+ Py_DECREF(qresources);
281
+ Py_DECREF(base_path_obj);
282
+ Py_DECREF(pathlib);
283
+ Py_DECREF(root_elem);
284
+ Py_DECREF(etree);
285
+ return -1;
286
+ }
287
+
288
+ long long lastmod = 0;
289
+ PyObject *stat_obj = PyObject_CallMethod(file_path_obj, "stat", NULL);
290
+ if (stat_obj) {
291
+ PyObject *mtime_obj = PyObject_GetAttrString(stat_obj, "st_mtime");
292
+ if (mtime_obj) {
293
+ double mtime = PyFloat_AsDouble(mtime_obj);
294
+ lastmod = (long long)(mtime * 1000.0);
295
+ Py_DECREF(mtime_obj);
296
+ }
297
+ Py_DECREF(stat_obj);
298
+ }
299
+ Py_DECREF(file_path_obj);
300
+
301
+ PyObject *alias_obj = PyObject_CallMethod(file_item, "get", "s", "alias");
302
+ const char *target = (alias_obj && alias_obj != Py_None) ? PyUnicode_AsUTF8(alias_obj) : rel_path;
303
+ while (*target == '/') target++;
304
+
305
+ size_t vpath_len = plen + strlen(target) + 2;
306
+ char *vpath = (char *)malloc(vpath_len);
307
+ if (vpath) {
308
+ if (plen > 0) {
309
+ snprintf(vpath, vpath_len, "%s/%s", clean_prefix, target);
310
+ } else {
311
+ snprintf(vpath, vpath_len, "%s", target);
312
+ }
313
+ for (char *c = vpath; *c; c++) {
314
+ if (*c == '\\') *c = '/';
315
+ }
316
+ add_to_tree(root, vpath, data_bytes, lastmod);
317
+ free(vpath);
318
+ }
319
+
320
+ Py_XDECREF(alias_obj);
321
+ Py_DECREF(data_bytes);
322
+ Py_DECREF(stripped_text);
323
+ }
324
+ Py_DECREF(files);
325
+ Py_XDECREF(prefix_obj);
326
+ }
327
+
328
+ Py_DECREF(qresources);
329
+ Py_DECREF(base_path_obj);
330
+ Py_DECREF(pathlib);
331
+ Py_DECREF(root_elem);
332
+ Py_DECREF(etree);
333
+ return 0;
334
+ }
335
+
336
+ static int compare_nodes(const void *a, const void *b) {
337
+ ResourceNode *const *node_a = (ResourceNode *const *)a;
338
+ ResourceNode *const *node_b = (ResourceNode *const *)b;
339
+ unsigned int hash_a = qt_hash((*node_a)->name, 0);
340
+ unsigned int hash_b = qt_hash((*node_b)->name, 0);
341
+ if (hash_a < hash_b) return -1;
342
+ if (hash_a > hash_b) return 1;
343
+ return strcmp((*node_a)->name, (*node_b)->name);
344
+ }
345
+
346
+ static void sort_tree(ResourceNode *node) {
347
+ if (node->child_count > 0) {
348
+ qsort((void *)node->children, (size_t)node->child_count, sizeof(ResourceNode *), compare_nodes);
349
+ for (int i = 0; i < node->child_count; i++) {
350
+ sort_tree(node->children[i]);
351
+ }
352
+ }
353
+ }
354
+
355
+ static void flatten_tree(ResourceNode *root, FlatList *fl) {
356
+ int queue_capacity = 1000;
357
+ ResourceNode **queue = (ResourceNode **)malloc(sizeof(ResourceNode *) * (size_t)queue_capacity);
358
+ if (!queue) return;
359
+ int head = 0, tail = 0;
360
+ queue[tail++] = root;
361
+
362
+ while (head < tail) {
363
+ ResourceNode *curr = queue[head++];
364
+
365
+ if (fl->count >= fl->capacity) {
366
+ int new_cap = fl->capacity == 0 ? 1000 : fl->capacity * 2;
367
+ ResourceNode **new_nodes = (ResourceNode **)realloc((void *)fl->nodes, sizeof(ResourceNode *) * (size_t)new_cap);
368
+ if (!new_nodes) {
369
+ free((void *)queue);
370
+ return;
371
+ }
372
+ fl->nodes = new_nodes;
373
+ fl->capacity = new_cap;
374
+ }
375
+ fl->nodes[fl->count++] = curr;
376
+
377
+ for (int i = 0; i < curr->child_count; i++) {
378
+ if (tail >= queue_capacity) {
379
+ int new_q_cap = queue_capacity * 2;
380
+ ResourceNode **new_q = (ResourceNode **)realloc((void *)queue, sizeof(ResourceNode *) * (size_t)new_q_cap);
381
+ if (!new_q) {
382
+ free((void *)queue);
383
+ return;
384
+ }
385
+ queue = new_q;
386
+ queue_capacity = new_q_cap;
387
+ }
388
+ queue[tail++] = curr->children[i];
389
+ }
390
+ }
391
+ free((void *)queue);
392
+ }
393
+
394
+ int compile_qrc(const char *xml_content, const char *base_dir, CompileResult *out) {
395
+ bb_init(&out->name_bytes);
396
+ bb_init(&out->data_bytes);
397
+ bb_init(&out->struct_v1_bytes);
398
+ bb_init(&out->struct_v2_bytes);
399
+ out->error[0] = '\0';
400
+
401
+ ResourceNode *root = create_node("", 1, NULL);
402
+ if (!root) {
403
+ snprintf(out->error, sizeof(out->error), "Failed to allocate root node");
404
+ return -1;
405
+ }
406
+
407
+ if (parse_qrc(xml_content, root, base_dir, out->error, sizeof(out->error)) != 0) {
408
+ free_node(root);
409
+ return -1;
410
+ }
411
+
412
+ sort_tree(root);
413
+
414
+ FlatList fl = {NULL, 0, 0};
415
+ flatten_tree(root, &fl);
416
+
417
+ for (int i = 0; i < fl.count; i++) {
418
+ ResourceNode *node = fl.nodes[i];
419
+ if (i == 0) {
420
+ node->name_offset = 0;
421
+ continue;
422
+ }
423
+ node->name_offset = (int)out->name_bytes.size;
424
+
425
+ PyObject *py_name = PyUnicode_FromString(node->name);
426
+ if (!py_name) {
427
+ snprintf(out->error, sizeof(out->error), "Invalid name: %s", node->name);
428
+ free((void *)fl.nodes);
429
+ free_node(root);
430
+ return -1;
431
+ }
432
+
433
+ PyObject *u16_bytes = PyUnicode_AsEncodedString(py_name, "utf-16be", "strict");
434
+ Py_DECREF(py_name);
435
+ if (!u16_bytes) {
436
+ snprintf(out->error, sizeof(out->error), "Failed to encode name to UTF-16: %s", node->name);
437
+ free((void *)fl.nodes);
438
+ free_node(root);
439
+ return -1;
440
+ }
441
+
442
+ char *u16_data = NULL;
443
+ Py_ssize_t u16_len = 0;
444
+ PyBytes_AsStringAndSize(u16_bytes, &u16_data, &u16_len);
445
+
446
+ unsigned short u16_units = (unsigned short)(u16_len / 2);
447
+ bb_append_u16be(&out->name_bytes, u16_units);
448
+ bb_append_u32be(&out->name_bytes, qt_hash(node->name, 0));
449
+ bb_append(&out->name_bytes, (const unsigned char *)u16_data, (size_t)u16_len);
450
+ Py_DECREF(u16_bytes);
451
+ }
452
+
453
+ PyObject *zlib_mod = PyImport_ImportModule("zlib");
454
+ if (!zlib_mod) {
455
+ snprintf(out->error, sizeof(out->error), "Failed to import Python zlib module");
456
+ free((void *)fl.nodes);
457
+ free_node(root);
458
+ return -1;
459
+ }
460
+
461
+ for (int i = 0; i < fl.count; i++) {
462
+ ResourceNode *node = fl.nodes[i];
463
+ if (node->is_dir) continue;
464
+
465
+ node->data_offset = (int)out->data_bytes.size;
466
+
467
+ char *raw_data = NULL;
468
+ Py_ssize_t raw_size = 0;
469
+ PyBytes_AsStringAndSize(node->data_bytes, &raw_data, &raw_size);
470
+
471
+ PyObject *comp_obj = PyObject_CallMethod(zlib_mod, "compress", "y#i", raw_data, raw_size, 9);
472
+ if (!comp_obj) {
473
+ PyErr_Clear();
474
+ snprintf(out->error, sizeof(out->error), "Failed to compress file data: %s", node->name);
475
+ Py_DECREF(zlib_mod);
476
+ free((void *)fl.nodes);
477
+ free_node(root);
478
+ return -1;
479
+ }
480
+
481
+ char *comp_data = NULL;
482
+ Py_ssize_t comp_size = 0;
483
+ PyBytes_AsStringAndSize(comp_obj, &comp_data, &comp_size);
484
+
485
+ size_t name_len = strlen(node->name);
486
+ int is_ico = (name_len >= 4 && strcmp(node->name + name_len - 4, ".ico") == 0);
487
+
488
+ if (is_ico || comp_size >= raw_size) {
489
+ node->flags = 0x0000;
490
+ bb_append_u32be(&out->data_bytes, (unsigned int)raw_size);
491
+ bb_append(&out->data_bytes, (const unsigned char *)raw_data, (size_t)raw_size);
492
+ } else {
493
+ node->flags = 0x0001;
494
+ unsigned int payload_size = (unsigned int)comp_size + 4;
495
+ bb_append_u32be(&out->data_bytes, payload_size);
496
+ bb_append_u32be(&out->data_bytes, (unsigned int)raw_size);
497
+ bb_append(&out->data_bytes, (const unsigned char *)comp_data, (size_t)comp_size);
498
+ }
499
+
500
+ Py_DECREF(comp_obj);
501
+ }
502
+
503
+ Py_DECREF(zlib_mod);
504
+
505
+ for (int i = 0; i < fl.count; i++) {
506
+ ResourceNode *node = fl.nodes[i];
507
+ unsigned int mix, offset = 0;
508
+
509
+ if (node->is_dir) {
510
+ node->flags = 2;
511
+ mix = (unsigned int)node->child_count;
512
+ if (node->child_count > 0) {
513
+ ResourceNode *first_child = node->children[0];
514
+ for (int j = 0; j < fl.count; j++) {
515
+ if (fl.nodes[j] == first_child) {
516
+ offset = (unsigned int)j;
517
+ break;
518
+ }
519
+ }
520
+ }
521
+ } else {
522
+ mix = 1;
523
+ offset = (unsigned int)node->data_offset;
524
+ }
525
+
526
+ bb_append_u32be(&out->struct_v1_bytes, (unsigned int)node->name_offset);
527
+ bb_append_u16be(&out->struct_v1_bytes, node->flags);
528
+ bb_append_u32be(&out->struct_v1_bytes, mix);
529
+ bb_append_u32be(&out->struct_v1_bytes, offset);
530
+
531
+ bb_append_u32be(&out->struct_v2_bytes, (unsigned int)node->name_offset);
532
+ bb_append_u16be(&out->struct_v2_bytes, node->flags);
533
+ bb_append_u32be(&out->struct_v2_bytes, mix);
534
+ bb_append_i32be(&out->struct_v2_bytes, (int)offset);
535
+ bb_append_u64be(&out->struct_v2_bytes, (unsigned long long)node->lastmod);
536
+ }
537
+
538
+ free((void *)fl.nodes);
539
+ free_node(root);
540
+ return 0;
541
+ }
qres-0.1.0/src/qres.c ADDED
@@ -0,0 +1,74 @@
1
+ #define PY_SSIZE_T_CLEAN
2
+ #include <Python.h>
3
+ #include "compiler.h"
4
+
5
+ static PyObject *py_compile(PyObject *self, PyObject *args) {
6
+ (void)self;
7
+ const char *xml_content;
8
+ const char *base_dir;
9
+
10
+ if (!PyArg_ParseTuple(args, "ss", &xml_content, &base_dir))
11
+ return NULL;
12
+
13
+ CompileResult result;
14
+ if (compile_qrc(xml_content, base_dir, &result) != 0) {
15
+ PyErr_SetString(PyExc_RuntimeError, result.error);
16
+ return NULL;
17
+ }
18
+
19
+ PyObject *data = PyBytes_FromStringAndSize((char *)result.data_bytes.data, (Py_ssize_t)result.data_bytes.size);
20
+ PyObject *name = PyBytes_FromStringAndSize((char *)result.name_bytes.data, (Py_ssize_t)result.name_bytes.size);
21
+ PyObject *struct_v1 = PyBytes_FromStringAndSize((char *)result.struct_v1_bytes.data, (Py_ssize_t)result.struct_v1_bytes.size);
22
+ PyObject *struct_v2 = PyBytes_FromStringAndSize((char *)result.struct_v2_bytes.data, (Py_ssize_t)result.struct_v2_bytes.size);
23
+
24
+ bb_free(&result.data_bytes);
25
+ bb_free(&result.name_bytes);
26
+ bb_free(&result.struct_v1_bytes);
27
+ bb_free(&result.struct_v2_bytes);
28
+
29
+ if (!data || !name || !struct_v1 || !struct_v2) {
30
+ Py_XDECREF(data);
31
+ Py_XDECREF(name);
32
+ Py_XDECREF(struct_v1);
33
+ Py_XDECREF(struct_v2);
34
+ return NULL;
35
+ }
36
+
37
+ PyObject *dict = PyDict_New();
38
+ PyDict_SetItemString(dict, "data", data);
39
+ PyDict_SetItemString(dict, "name", name);
40
+ PyDict_SetItemString(dict, "struct_v1", struct_v1);
41
+ PyDict_SetItemString(dict, "struct_v2", struct_v2);
42
+
43
+ Py_DECREF(data);
44
+ Py_DECREF(name);
45
+ Py_DECREF(struct_v1);
46
+ Py_DECREF(struct_v2);
47
+
48
+ return dict;
49
+ }
50
+
51
+ static PyMethodDef qres_methods[] = {
52
+ {"compile", py_compile, METH_VARARGS,
53
+ "compile(xml: str, base_dir: str) -> dict\n\n"
54
+ "Compile a .qrc XML string into Qt resource blobs.\n\n"
55
+ "Returns a dict with keys: 'data', 'name', 'struct_v1', 'struct_v2'.\n"
56
+ "base_dir is the directory used to resolve relative file paths in the XML."},
57
+ {NULL, NULL, 0, NULL}
58
+ };
59
+
60
+ static struct PyModuleDef qres_module = {
61
+ PyModuleDef_HEAD_INIT,
62
+ "qres._qres",
63
+ "Qt resource compiler C extension.",
64
+ -1,
65
+ qres_methods,
66
+ NULL,
67
+ NULL,
68
+ NULL,
69
+ NULL
70
+ };
71
+
72
+ PyMODINIT_FUNC PyInit__qres(void) {
73
+ return PyModule_Create(&qres_module);
74
+ }
@@ -0,0 +1,160 @@
1
+ from pathlib import Path
2
+ import pytest
3
+ import qres
4
+
5
+
6
+ def test_compile_basic(tmp_path: Path) -> None:
7
+ asset = tmp_path / "hello.txt"
8
+ asset.write_text("Hello Qt Resources!", encoding="utf-8")
9
+
10
+ qrc = f"""<RCC>
11
+ <qresource>
12
+ <file>{asset.name}</file>
13
+ </qresource>
14
+ </RCC>"""
15
+
16
+ blobs = qres.compile(qrc, str(tmp_path))
17
+ assert "data" in blobs
18
+ assert "name" in blobs
19
+ assert "struct_v1" in blobs
20
+ assert "struct_v2" in blobs
21
+ assert len(blobs["data"]) > 0
22
+ assert len(blobs["name"]) > 0
23
+
24
+
25
+ def test_compile_with_alias(tmp_path: Path) -> None:
26
+ asset = tmp_path / "actual_file.txt"
27
+ asset.write_text("Aliased Content", encoding="utf-8")
28
+
29
+ qrc = f"""<RCC>
30
+ <qresource>
31
+ <file alias="virtual_alias.txt">{asset.name}</file>
32
+ </qresource>
33
+ </RCC>"""
34
+
35
+ blobs = qres.compile(qrc, str(tmp_path))
36
+ assert len(blobs["data"]) > 0
37
+ # In Qt name table, "virtual_alias.txt" is encoded in UTF-16BE
38
+ alias_bytes = "virtual_alias.txt".encode("utf-16-be")
39
+ assert alias_bytes in blobs["name"]
40
+
41
+
42
+ def test_compile_with_prefix(tmp_path: Path) -> None:
43
+ asset = tmp_path / "icon.png"
44
+ asset.write_bytes(b"\x89PNG\r\n\x1a\nfake_png_data")
45
+
46
+ qrc = f"""<RCC>
47
+ <qresource prefix="/icons/toolbar">
48
+ <file>{asset.name}</file>
49
+ </qresource>
50
+ </RCC>"""
51
+
52
+ blobs = qres.compile(qrc, str(tmp_path))
53
+ assert len(blobs["data"]) > 0
54
+ icons_bytes = "icons".encode("utf-16-be")
55
+ toolbar_bytes = "toolbar".encode("utf-16-be")
56
+ icon_name_bytes = "icon.png".encode("utf-16-be")
57
+ assert icons_bytes in blobs["name"]
58
+ assert toolbar_bytes in blobs["name"]
59
+ assert icon_name_bytes in blobs["name"]
60
+
61
+
62
+ def test_compile_ignores_comments(tmp_path: Path) -> None:
63
+ asset = tmp_path / "kept.txt"
64
+ asset.write_text("Keep me", encoding="utf-8")
65
+
66
+ qrc = f"""<RCC>
67
+ <!-- This is a comment: <file>missing.txt</file> -->
68
+ <qresource>
69
+ <file>{asset.name}</file>
70
+ </qresource>
71
+ </RCC>"""
72
+
73
+ blobs = qres.compile(qrc, str(tmp_path))
74
+ assert len(blobs["data"]) > 0
75
+ missing_bytes = "missing.txt".encode("utf-16-be")
76
+ assert missing_bytes not in blobs["name"]
77
+
78
+
79
+ def test_compile_utf8_filename(tmp_path: Path) -> None:
80
+ asset = tmp_path / "café_res.txt"
81
+ asset.write_text("Coffee", encoding="utf-8")
82
+
83
+ qrc = f"""<RCC>
84
+ <qresource>
85
+ <file>{asset.name}</file>
86
+ </qresource>
87
+ </RCC>"""
88
+
89
+ blobs = qres.compile(qrc, str(tmp_path))
90
+ expected_utf16be = asset.name.encode("utf-16-be")
91
+ assert expected_utf16be in blobs["name"]
92
+
93
+
94
+ def test_compile_malformed_xml_raises(tmp_path: Path) -> None:
95
+ qrc = "<RCC><qresource><file>unclosed"
96
+ with pytest.raises(RuntimeError, match="XML syntax error"):
97
+ qres.compile(qrc, str(tmp_path))
98
+
99
+
100
+ def test_compile_missing_file_raises(tmp_path: Path) -> None:
101
+ qrc = """<RCC>
102
+ <qresource>
103
+ <file>non_existent_asset.txt</file>
104
+ </qresource>
105
+ </RCC>"""
106
+ with pytest.raises(RuntimeError, match="Cannot open file"):
107
+ qres.compile(qrc, str(tmp_path))
108
+
109
+
110
+ def test_compile_file_and_binding(tmp_path: Path) -> None:
111
+ asset = tmp_path / "data.bin"
112
+ asset.write_bytes(b"\x00\x01\x02\x03")
113
+
114
+ qrc_file = tmp_path / "test.qrc"
115
+ qrc_file.write_text(
116
+ f"""<RCC>
117
+ <qresource prefix="assets">
118
+ <file>{asset.name}</file>
119
+ </qresource>
120
+ </RCC>""",
121
+ encoding="utf-8",
122
+ )
123
+
124
+ out_file = tmp_path / "test_rc.py"
125
+ qres.compile_file(qrc_file, out_file, binding="PySide6")
126
+
127
+ assert out_file.exists()
128
+ content = out_file.read_text(encoding="utf-8")
129
+ assert "from PySide6 import QtCore" in content
130
+ assert "qt_resource_data = b\"" in content
131
+ assert "qt_resource_name = b\"" in content
132
+ assert "qt_resource_struct_v1 = b\"" in content
133
+ assert "qt_resource_struct_v2 = b\"" in content
134
+ assert "qInitResources()" in content
135
+ assert "qCleanupResources()" in content
136
+
137
+
138
+ def test_compile_file_default_auto_binding(tmp_path: Path) -> None:
139
+ asset = tmp_path / "data.bin"
140
+ asset.write_bytes(b"\x00\x01")
141
+
142
+ qrc_file = tmp_path / "test.qrc"
143
+ qrc_file.write_text(
144
+ f"""<RCC>
145
+ <qresource>
146
+ <file>{asset.name}</file>
147
+ </qresource>
148
+ </RCC>""",
149
+ encoding="utf-8",
150
+ )
151
+
152
+ out_file = tmp_path / "auto_rc.py"
153
+ qres.compile_file(qrc_file, out_file)
154
+
155
+ assert out_file.exists()
156
+ content = out_file.read_text(encoding="utf-8")
157
+ assert "except ImportError:" in content
158
+ assert "from PyQt6 import QtCore" in content
159
+ assert "from qtpy import QtCore" in content
160
+