osz2 1.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.
- osz2-1.1.0/LICENSE +8 -0
- osz2-1.1.0/PKG-INFO +174 -0
- osz2-1.1.0/README.md +158 -0
- osz2-1.1.0/osz2/__init__.py +17 -0
- osz2-1.1.0/osz2/__main__.py +186 -0
- osz2-1.1.0/osz2/constants.py +29 -0
- osz2-1.1.0/osz2/crypto.c +448 -0
- osz2-1.1.0/osz2/file.py +33 -0
- osz2-1.1.0/osz2/keys.py +36 -0
- osz2-1.1.0/osz2/metadata.py +27 -0
- osz2-1.1.0/osz2/package.py +418 -0
- osz2-1.1.0/osz2/patch.py +42 -0
- osz2-1.1.0/osz2/simple_cryptor.py +16 -0
- osz2-1.1.0/osz2/utils.py +117 -0
- osz2-1.1.0/osz2/xtea.py +70 -0
- osz2-1.1.0/osz2/xxtea.py +130 -0
- osz2-1.1.0/osz2/xxtea_reader.py +22 -0
- osz2-1.1.0/osz2/xxtea_writer.py +35 -0
- osz2-1.1.0/osz2.egg-info/PKG-INFO +174 -0
- osz2-1.1.0/osz2.egg-info/SOURCES.txt +25 -0
- osz2-1.1.0/osz2.egg-info/dependency_links.txt +1 -0
- osz2-1.1.0/osz2.egg-info/entry_points.txt +2 -0
- osz2-1.1.0/osz2.egg-info/requires.txt +2 -0
- osz2-1.1.0/osz2.egg-info/top_level.txt +1 -0
- osz2-1.1.0/pyproject.toml +29 -0
- osz2-1.1.0/setup.cfg +4 -0
- osz2-1.1.0/setup.py +10 -0
osz2-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
Copyright (c) 2025 Levi <contact@lekuru.xyz>, ascenttree
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
osz2-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osz2
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A python library for reading osz2 files
|
|
5
|
+
Author: ascenttree
|
|
6
|
+
Author-email: Lekuru <contact@lekuru.xyz>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/Lekuruu/osz2.py
|
|
9
|
+
Keywords: osu,osz2,python,bancho
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: bsdiff4
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# osz2.py
|
|
18
|
+
|
|
19
|
+
[](https://www.python.org/)
|
|
20
|
+
[](https://github.com/Lekuruu/osz2.py/blob/main/LICENSE)
|
|
21
|
+
[](https://github.com/Lekuruu/osz2.py/actions/workflows/build.yml)
|
|
22
|
+
|
|
23
|
+
osz2.py is a Python library for reading osz2 files. It's a direct port of the existing [Osz2Decryptor](https://github.com/xxCherry/Osz2Decryptor) project by [xxCherry](https://github.com/xxCherry) and [osz2-go](https://github.com/Lekuruu/osz2-go) by me. The Python port itself was done by [@ascenttree](https://github.com/ascenttree); all credit goes to them. I took part in code refactoring and optimizing the performance by moving the heavy crypto primitives into a native C extension (`osz2.crypto`) on top of [NumPy](https://numpy.org/), bringing encryption time down to ~100 ms instead of 25 seconds.
|
|
24
|
+
|
|
25
|
+
This project *won't* provide beatmap parsing support. You will have to implement that by yourself, if you decide to use this library for implementing the beatmap submission system.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install osz2
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install from source:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/Lekuruu/osz2.py
|
|
37
|
+
cd osz2.py
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
This repository provides a command-line interface for easy testing:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m osz2 decrypt <input.osz2> <output_directory>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m osz2 encrypt <target_directory> <output.osz2>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
But that's not all!
|
|
54
|
+
Here is an example of how to use osz2.py as a library:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from osz2 import Osz2Package, MetadataType
|
|
58
|
+
|
|
59
|
+
# Parse package from file
|
|
60
|
+
package = Osz2Package.from_file("beatmap.osz2")
|
|
61
|
+
|
|
62
|
+
# Access metadata
|
|
63
|
+
print("Title:", package.metadata.get(MetadataType.Title))
|
|
64
|
+
print("Artist:", package.metadata.get(MetadataType.Artist))
|
|
65
|
+
print("Creator:", package.metadata.get(MetadataType.Creator))
|
|
66
|
+
print("Difficulty:", package.metadata.get(MetadataType.Difficulty))
|
|
67
|
+
|
|
68
|
+
# Access files
|
|
69
|
+
for file in package.files:
|
|
70
|
+
print(f"File: {file.filename}, Size: {len(file.content)} bytes")
|
|
71
|
+
|
|
72
|
+
# Extract specific files
|
|
73
|
+
for file in package.files:
|
|
74
|
+
if not file.filename.endswith(".osu"):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
with open(file.filename, "wb") as f:
|
|
78
|
+
f.write(file.content)
|
|
79
|
+
|
|
80
|
+
# Create a regular .osz package
|
|
81
|
+
with open("beatmap.osz", "wb") as f:
|
|
82
|
+
f.write(package.create_osz_package())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Metadata-only Mode
|
|
86
|
+
|
|
87
|
+
If you only need to read metadata without extracting files, you can use the `metadata_only` parameter:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# Only parse metadata
|
|
91
|
+
package = Osz2Package.from_file("beatmap.osz2", metadata_only=True)
|
|
92
|
+
|
|
93
|
+
# Access metadata
|
|
94
|
+
print("Title:", package.metadata.get(MetadataType.Title))
|
|
95
|
+
print("BeatmapSet ID:", package.metadata.get(MetadataType.BeatmapSetID))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Alternative Constructors
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# From file path
|
|
102
|
+
package = Osz2Package.from_file("beatmap.osz2")
|
|
103
|
+
|
|
104
|
+
# From bytes
|
|
105
|
+
with open("beatmap.osz2", "rb") as f:
|
|
106
|
+
data = f.read()
|
|
107
|
+
package = Osz2Package.from_bytes(data)
|
|
108
|
+
|
|
109
|
+
# From an io.BufferedReader-like object, e.g. a file stream
|
|
110
|
+
with open("beatmap.osz2", "rb") as f:
|
|
111
|
+
package = Osz2Package(f)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Exporting an osz2 package
|
|
115
|
+
|
|
116
|
+
You can initialize and export osz2 packages from a directory:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from osz2 import Osz2Package, MetadataType
|
|
120
|
+
|
|
121
|
+
# Initialize package from a directory containing beatmap files
|
|
122
|
+
package = Osz2Package.from_directory("./my_beatmap_folder")
|
|
123
|
+
|
|
124
|
+
# Export to osz2 format
|
|
125
|
+
osz2_data = package.export()
|
|
126
|
+
|
|
127
|
+
with open("output.osz2", "wb") as f:
|
|
128
|
+
f.write(osz2_data)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Applying a patch
|
|
132
|
+
|
|
133
|
+
When developing an implementation of the beatmap submission system, this could come in handy:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Assuming you have a source osz2 file and a patch file
|
|
137
|
+
osz2_file = b"..."
|
|
138
|
+
patch_file = b"..."
|
|
139
|
+
|
|
140
|
+
updated_osz2 = osz2.apply_bsdiff_patch(osz2_file, patch_file)
|
|
141
|
+
osz2 = Osz2Package.from_bytes(updated_osz2)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Using osu!stream .osf2 files
|
|
145
|
+
|
|
146
|
+
I have not tested this, but in theory this should work by passing in `KeyType.OSF2` when initializing the osz2 package:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
osf2 = Osz2Package.from_file("beatmap.osf2", key_type=KeyType.OSF2)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
You can also specify this when using the command-line interface:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
python -m osz2 <input.osz2> <output_directory> --key-type osf2
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Building the crypto.c extension
|
|
159
|
+
|
|
160
|
+
If you change any code under `osz2/crypto.c`, rebuild the module in place before running tests:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
python -m venv venv
|
|
164
|
+
source venv/bin/activate
|
|
165
|
+
pip install -e .
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or, if you only need the extension locally:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python setup.py build_ext --inplace
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The resulting shared object is picked up automatically by the package import system.
|
osz2-1.1.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# osz2.py
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](https://github.com/Lekuruu/osz2.py/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/Lekuruu/osz2.py/actions/workflows/build.yml)
|
|
6
|
+
|
|
7
|
+
osz2.py is a Python library for reading osz2 files. It's a direct port of the existing [Osz2Decryptor](https://github.com/xxCherry/Osz2Decryptor) project by [xxCherry](https://github.com/xxCherry) and [osz2-go](https://github.com/Lekuruu/osz2-go) by me. The Python port itself was done by [@ascenttree](https://github.com/ascenttree); all credit goes to them. I took part in code refactoring and optimizing the performance by moving the heavy crypto primitives into a native C extension (`osz2.crypto`) on top of [NumPy](https://numpy.org/), bringing encryption time down to ~100 ms instead of 25 seconds.
|
|
8
|
+
|
|
9
|
+
This project *won't* provide beatmap parsing support. You will have to implement that by yourself, if you decide to use this library for implementing the beatmap submission system.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install osz2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install from source:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/Lekuruu/osz2.py
|
|
21
|
+
cd osz2.py
|
|
22
|
+
pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
This repository provides a command-line interface for easy testing:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m osz2 decrypt <input.osz2> <output_directory>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m osz2 encrypt <target_directory> <output.osz2>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
But that's not all!
|
|
38
|
+
Here is an example of how to use osz2.py as a library:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from osz2 import Osz2Package, MetadataType
|
|
42
|
+
|
|
43
|
+
# Parse package from file
|
|
44
|
+
package = Osz2Package.from_file("beatmap.osz2")
|
|
45
|
+
|
|
46
|
+
# Access metadata
|
|
47
|
+
print("Title:", package.metadata.get(MetadataType.Title))
|
|
48
|
+
print("Artist:", package.metadata.get(MetadataType.Artist))
|
|
49
|
+
print("Creator:", package.metadata.get(MetadataType.Creator))
|
|
50
|
+
print("Difficulty:", package.metadata.get(MetadataType.Difficulty))
|
|
51
|
+
|
|
52
|
+
# Access files
|
|
53
|
+
for file in package.files:
|
|
54
|
+
print(f"File: {file.filename}, Size: {len(file.content)} bytes")
|
|
55
|
+
|
|
56
|
+
# Extract specific files
|
|
57
|
+
for file in package.files:
|
|
58
|
+
if not file.filename.endswith(".osu"):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
with open(file.filename, "wb") as f:
|
|
62
|
+
f.write(file.content)
|
|
63
|
+
|
|
64
|
+
# Create a regular .osz package
|
|
65
|
+
with open("beatmap.osz", "wb") as f:
|
|
66
|
+
f.write(package.create_osz_package())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Metadata-only Mode
|
|
70
|
+
|
|
71
|
+
If you only need to read metadata without extracting files, you can use the `metadata_only` parameter:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Only parse metadata
|
|
75
|
+
package = Osz2Package.from_file("beatmap.osz2", metadata_only=True)
|
|
76
|
+
|
|
77
|
+
# Access metadata
|
|
78
|
+
print("Title:", package.metadata.get(MetadataType.Title))
|
|
79
|
+
print("BeatmapSet ID:", package.metadata.get(MetadataType.BeatmapSetID))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Alternative Constructors
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# From file path
|
|
86
|
+
package = Osz2Package.from_file("beatmap.osz2")
|
|
87
|
+
|
|
88
|
+
# From bytes
|
|
89
|
+
with open("beatmap.osz2", "rb") as f:
|
|
90
|
+
data = f.read()
|
|
91
|
+
package = Osz2Package.from_bytes(data)
|
|
92
|
+
|
|
93
|
+
# From an io.BufferedReader-like object, e.g. a file stream
|
|
94
|
+
with open("beatmap.osz2", "rb") as f:
|
|
95
|
+
package = Osz2Package(f)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Exporting an osz2 package
|
|
99
|
+
|
|
100
|
+
You can initialize and export osz2 packages from a directory:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from osz2 import Osz2Package, MetadataType
|
|
104
|
+
|
|
105
|
+
# Initialize package from a directory containing beatmap files
|
|
106
|
+
package = Osz2Package.from_directory("./my_beatmap_folder")
|
|
107
|
+
|
|
108
|
+
# Export to osz2 format
|
|
109
|
+
osz2_data = package.export()
|
|
110
|
+
|
|
111
|
+
with open("output.osz2", "wb") as f:
|
|
112
|
+
f.write(osz2_data)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Applying a patch
|
|
116
|
+
|
|
117
|
+
When developing an implementation of the beatmap submission system, this could come in handy:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Assuming you have a source osz2 file and a patch file
|
|
121
|
+
osz2_file = b"..."
|
|
122
|
+
patch_file = b"..."
|
|
123
|
+
|
|
124
|
+
updated_osz2 = osz2.apply_bsdiff_patch(osz2_file, patch_file)
|
|
125
|
+
osz2 = Osz2Package.from_bytes(updated_osz2)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Using osu!stream .osf2 files
|
|
129
|
+
|
|
130
|
+
I have not tested this, but in theory this should work by passing in `KeyType.OSF2` when initializing the osz2 package:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
osf2 = Osz2Package.from_file("beatmap.osf2", key_type=KeyType.OSF2)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
You can also specify this when using the command-line interface:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python -m osz2 <input.osz2> <output_directory> --key-type osf2
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Building the crypto.c extension
|
|
143
|
+
|
|
144
|
+
If you change any code under `osz2/crypto.c`, rebuild the module in place before running tests:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python -m venv venv
|
|
148
|
+
source venv/bin/activate
|
|
149
|
+
pip install -e .
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Or, if you only need the extension locally:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
python setup.py build_ext --inplace
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The resulting shared object is picked up automatically by the package import system.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
__author__ = "Lekuru"
|
|
3
|
+
__email__ = "contact@lekuru.xyz"
|
|
4
|
+
__version__ = "1.1.0"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
|
|
7
|
+
from .patch import apply_bsdiff_patch
|
|
8
|
+
from .metadata import MetadataType
|
|
9
|
+
from .package import Osz2Package
|
|
10
|
+
from .keys import KeyType
|
|
11
|
+
from .file import File
|
|
12
|
+
|
|
13
|
+
from .simple_cryptor import SimpleCryptor
|
|
14
|
+
from .xxtea_reader import XXTEAReader
|
|
15
|
+
from .xxtea_writer import XXTEAWriter
|
|
16
|
+
from .xxtea import XXTEA
|
|
17
|
+
from .xtea import XTEA
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Tuple, Dict, Union, Optional
|
|
3
|
+
from osz2.metadata import MetadataType
|
|
4
|
+
from osz2.package import Osz2Package
|
|
5
|
+
from osz2.keys import KeyType
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="osz2", description="A tool to decrypt, extract, and create osz2 files")
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
15
|
+
|
|
16
|
+
decrypt_parser = subparsers.add_parser("decrypt", help="Decrypt and extract an osz2 file")
|
|
17
|
+
decrypt_parser.add_argument("input", help="The path to the osz2 file to decrypt")
|
|
18
|
+
decrypt_parser.add_argument("output", help="The path to put the extracted files")
|
|
19
|
+
decrypt_parser.add_argument("--key-type", choices=["osz2", "osf2"], default="osz2", help="The key generation method")
|
|
20
|
+
decrypt_parser.add_argument("--create-osz", action="store_true", help="Also create a regular .osz package")
|
|
21
|
+
|
|
22
|
+
encrypt_parser = subparsers.add_parser("encrypt", help="Create an osz2 package from a directory")
|
|
23
|
+
encrypt_parser.add_argument("input", help="The path to the directory containing files")
|
|
24
|
+
encrypt_parser.add_argument("output", help="The output path for the osz2 file")
|
|
25
|
+
encrypt_parser.add_argument("--key-type", choices=["osz2", "osf2"], default="osz2", help="The key generation method")
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
if args.command is None:
|
|
30
|
+
parser.print_help()
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
key_type = KeyType(args.key_type)
|
|
34
|
+
|
|
35
|
+
if args.command == "decrypt":
|
|
36
|
+
osz2 = decrypt_osz2(args.input, key_type)
|
|
37
|
+
save_osz2(osz2, args.output)
|
|
38
|
+
|
|
39
|
+
if not args.create_osz:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
osz_data = osz2.create_osz_package(exclude_disallowed_files=False)
|
|
43
|
+
with open(f"{args.output}/{osz2.osz_filename}", "wb") as f:
|
|
44
|
+
f.write(osz_data)
|
|
45
|
+
|
|
46
|
+
elif args.command == "encrypt":
|
|
47
|
+
encrypt_directory(
|
|
48
|
+
args.input,
|
|
49
|
+
args.output,
|
|
50
|
+
key_type
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def decrypt_osz2(filepath: str, key_type: KeyType) -> Osz2Package:
|
|
54
|
+
if not os.path.exists(filepath):
|
|
55
|
+
print(f"Error: Input file does not exist: {filepath}", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
print("Reading osz2 package...")
|
|
59
|
+
return Osz2Package.from_file(filepath, key_type=key_type)
|
|
60
|
+
|
|
61
|
+
def save_osz2(package: Osz2Package, output: str) -> None:
|
|
62
|
+
Path(output).mkdir(exist_ok=True)
|
|
63
|
+
print(f"Extracting {len(package.files)} files to {output}")
|
|
64
|
+
|
|
65
|
+
for file in package.files:
|
|
66
|
+
output_path = os.path.join(output, file.filename)
|
|
67
|
+
|
|
68
|
+
if (dir := Path(output_path).parent) != ".":
|
|
69
|
+
dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
with open(output_path, "wb") as f:
|
|
72
|
+
f.write(file.content)
|
|
73
|
+
|
|
74
|
+
print(f" -> {file.filename} ({len(file.content)} bytes)")
|
|
75
|
+
|
|
76
|
+
def encrypt_directory(
|
|
77
|
+
directory: str,
|
|
78
|
+
output: str,
|
|
79
|
+
key_type: KeyType
|
|
80
|
+
) -> None:
|
|
81
|
+
if not os.path.exists(directory):
|
|
82
|
+
print(f"Error: Input directory does not exist: {directory}", file=sys.stderr)
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
print(f"Creating osz2 package from directory: {directory}")
|
|
86
|
+
package = Osz2Package.from_directory(directory, key_type)
|
|
87
|
+
|
|
88
|
+
# Try to parse beatmap metadata and apply it to package
|
|
89
|
+
for beatmap in package.beatmap_files:
|
|
90
|
+
_, content = parse_beatmap(beatmap.content.decode("utf-8-sig"))
|
|
91
|
+
beatmap_id = content.get('Metadata', {}).get('BeatmapID', None)
|
|
92
|
+
|
|
93
|
+
if beatmap_id is not None:
|
|
94
|
+
package.beatmap_ids[beatmap.filename] = int(beatmap_id)
|
|
95
|
+
|
|
96
|
+
apply_metadata(package, content)
|
|
97
|
+
|
|
98
|
+
print(f"Exporting package with {len(package.files)} files...")
|
|
99
|
+
|
|
100
|
+
with open(output, "wb") as f:
|
|
101
|
+
data = package.export()
|
|
102
|
+
f.write(data)
|
|
103
|
+
|
|
104
|
+
print(f"Saved to: {output}")
|
|
105
|
+
|
|
106
|
+
def apply_metadata(package: Osz2Package, beatmap: Dict[str, dict]) -> None:
|
|
107
|
+
if 'Metadata' not in beatmap:
|
|
108
|
+
print("Error: No 'Metadata' section found in beatmap")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
if 'General' not in beatmap:
|
|
112
|
+
print("Error: No 'General' section found in beatmap")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
metadata_section = beatmap['Metadata']
|
|
116
|
+
title = metadata_section.get('TitleUnicode') or metadata_section.get('Title', '')
|
|
117
|
+
artist = metadata_section.get('ArtistUnicode') or metadata_section.get('Artist', '')
|
|
118
|
+
creator = metadata_section.get('Creator', '')
|
|
119
|
+
source = metadata_section.get('Source', '')
|
|
120
|
+
tags = metadata_section.get('Tags', '')
|
|
121
|
+
beatmapset_id = metadata_section.get('BeatmapSetID', -1)
|
|
122
|
+
|
|
123
|
+
general_section = beatmap['General']
|
|
124
|
+
preview_time = general_section.get('PreviewTime', 0)
|
|
125
|
+
|
|
126
|
+
package.metadata[MetadataType.Title] = str(title)
|
|
127
|
+
package.metadata[MetadataType.Artist] = str(artist)
|
|
128
|
+
package.metadata[MetadataType.Creator] = str(creator)
|
|
129
|
+
package.metadata[MetadataType.Source] = str(source)
|
|
130
|
+
package.metadata[MetadataType.Tags] = str(tags)
|
|
131
|
+
package.metadata[MetadataType.BeatmapSetID] = str(beatmapset_id)
|
|
132
|
+
package.metadata[MetadataType.PreviewTime] = str(preview_time)
|
|
133
|
+
|
|
134
|
+
def parse_beatmap(content: str) -> Tuple[int, Dict[str, dict]]:
|
|
135
|
+
sections: Dict[str, Union[dict, list]] = {}
|
|
136
|
+
current_section = None
|
|
137
|
+
beatmap_version = 0
|
|
138
|
+
|
|
139
|
+
for line in content.splitlines():
|
|
140
|
+
if line.startswith('osu file format'):
|
|
141
|
+
beatmap_version = int(line.removeprefix('osu file format v'))
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if (line.startswith('[') and line.endswith(']')):
|
|
145
|
+
# New section
|
|
146
|
+
current_section = line.removeprefix('[').removesuffix(']')
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if current_section is None:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if not line:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if current_section in ('General', 'Editor', 'Metadata', 'Difficulty'):
|
|
156
|
+
if current_section not in sections:
|
|
157
|
+
sections[current_section] = {}
|
|
158
|
+
|
|
159
|
+
# Parse key, value pair
|
|
160
|
+
key, value = (
|
|
161
|
+
split.strip() for split in line.split(':', maxsplit=1)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Try to parse float/int
|
|
165
|
+
value = parse_number(value) or value
|
|
166
|
+
|
|
167
|
+
sections[current_section][key] = value
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if current_section not in sections:
|
|
171
|
+
sections[current_section] = []
|
|
172
|
+
|
|
173
|
+
# Append to list
|
|
174
|
+
sections[current_section].append(line)
|
|
175
|
+
|
|
176
|
+
return beatmap_version, sections
|
|
177
|
+
|
|
178
|
+
def parse_number(value: str) -> Optional[Union[int, float]]:
|
|
179
|
+
for cast in (int, float):
|
|
180
|
+
try:
|
|
181
|
+
return cast(value.strip())
|
|
182
|
+
except ValueError:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
# "knownPlain" constant derived from FastRandom(1990)
|
|
3
|
+
# https://github.com/ppy/osu-stream/blob/master/osu!stream/Helpers/osu!common/MapPackage.cs#L64
|
|
4
|
+
KNOWN_PLAIN = bytearray([
|
|
5
|
+
0x55, 0xAA, 0x74, 0x10, 0x2B, 0x56, 0xB3, 0x9E,
|
|
6
|
+
0x25, 0x9E, 0xFE, 0xB7, 0xBE, 0x06, 0xFC, 0xF2,
|
|
7
|
+
0xB6, 0x3C, 0x6F, 0x47, 0x7E, 0x38, 0x69, 0x43,
|
|
8
|
+
0x80, 0x89, 0x25, 0x00, 0xCC, 0xB6, 0xFE, 0x12,
|
|
9
|
+
0xA9, 0xB2, 0x4A, 0x2C, 0x96, 0xD5, 0xEA, 0x26,
|
|
10
|
+
0x42, 0x31, 0xAF, 0x0A, 0x0D, 0xAE, 0x00, 0xED,
|
|
11
|
+
0xFE, 0x96, 0xA6, 0x94, 0x99, 0xA7, 0x90, 0xE4,
|
|
12
|
+
0x68, 0xBF, 0xC6, 0x97, 0x5B, 0x1B, 0x5E, 0x7F
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
# A list of all allowed file extensions in an .osz package
|
|
16
|
+
# Did not cause any issues when testing on titanic
|
|
17
|
+
ALLOWED_FILE_EXTENSIONS = (
|
|
18
|
+
"osu", "osz", "osb", "osk", "png", "mp3",
|
|
19
|
+
"wav", "ogg", "jpg", "wmv", "flv", "flac",
|
|
20
|
+
"avi", "ini", "m4v", "mpg", "mov", "webm",
|
|
21
|
+
"ogv", "mpeg", "3gp", "mkv", "mp4", "jpeg",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# osu! officially only uses ".avi", ".flv" and ".mpg" for
|
|
25
|
+
# video files, so lets hope this won't cause any issues
|
|
26
|
+
VIDEO_FILE_EXTENSIONS = (
|
|
27
|
+
"wmv", "flv", "avi", "m4v", "mpg", "mov",
|
|
28
|
+
"webm", "ogv", "mpeg", "3gp", "mkv", "mp4",
|
|
29
|
+
)
|