Skip to content

Commit 4cc947e

Browse files
committed
Optimized WiiFS for speed
1 parent e04d9b0 commit 4cc947e

5 files changed

Lines changed: 49 additions & 24 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ with open(target_path, 'rb') as target_file:
5858
```
5959

6060
### [`WiiFS`](https://niema.net/NiemaFS/#niemafs.WiiFS) — Nintendo Wii DVD
61-
Note that, due to the need to decrypt the filesystem, this is extremely slow and memory-intensive (each decrypted partition is loaded into memory). Optimizations are possible and may be explored in the future.
61+
Note that, due to the need to decrypt the filesystem, this is extremely memory-intensive (each partition is loaded into memory to process in parallel for speed).
6262

6363
```python
6464
from niemafs import WiiFS

niemafs/__init__.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
#! /usr/bin/env python
2-
from niemafs.common import clean_string, FileSystem, open_file
2+
# standard imports
3+
from warnings import warn
4+
5+
# NiemaFS imports
6+
from niemafs.common import clean_string, FileSystem, open_file, safename
37
from niemafs.dir import DirFS
48
from niemafs.gcm import GcmFS
59
from niemafs.iso import IsoFS
6-
from niemafs.wii import WiiFS
710
from niemafs.zip import ZipFS
11+
12+
# build __all__
813
__all__ = [
9-
'clean_string', 'FileSystem', 'open_file', # common.py
10-
'DirFS', # dir.py
11-
'GcmFS', # gcm.py
12-
'IsoFS', # iso.py
13-
'WiiFS', # wii.py
14-
'ZipFS', # zip.py
14+
'clean_string', 'FileSystem', 'open_file', 'safename', # common.py
15+
'DirFS', # dir.py
16+
'GcmFS', # gcm.py
17+
'IsoFS', # iso.py
18+
'ZipFS', # zip.py
1519
]
20+
21+
# WiiFS depends on PyCryptodome, so import it afterwards
22+
try:
23+
from niemafs.wii import WiiFS
24+
__all__.append('WiiFS')
25+
except:
26+
warn("Unable to import WiiFS. Ensure that you have PyCryptodome installed")

niemafs/common.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,33 @@
1313
# constants
1414
DEFAULT_BUFFER_SIZE = 8388608 # 8 MB
1515
DEFAULT_COMPRESS_LEVEL = 9
16+
SAFE_CHARS = set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-')
1617

1718
def clean_string(s):
18-
'''Clean a string (binary or normal) by right-stripping 0x00 and spaces
19+
'''Clean a string (binary or normal) by right-stripping 0x00 and spaces.
1920
2021
Args:
21-
`s` (`bytes`): The ISO 9660 string to clean
22+
`s` (`bytes`): The ISO 9660 string to clean.
2223
2324
Returns:
24-
`str`: The cleaned string
25+
`str`: The cleaned string.
2526
'''
2627
if isinstance(s, bytes):
2728
return s.rstrip(b'\x00').decode().rstrip()
2829
else:
2930
return s.rstrip()
3031

32+
def safename(s):
33+
'''Convert a string into a version that is safe for a filename
34+
35+
Args:
36+
`s` (`str`): The original string.
37+
38+
Returns:
39+
`str`: A version of `s` that is safe for a filename.
40+
'''
41+
return ''.join(c if c in SAFE_CHARS else '_' for c in s)
42+
3143
def open_file(path, mode='rb', buffering=DEFAULT_BUFFER_SIZE, compresslevel=DEFAULT_COMPRESS_LEVEL):
3244
'''Open a file for reading, writing, or appending. Automatically handles GZIP compression.
3345

niemafs/wii.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
'''
55

66
# NiemaFS imports
7-
from niemafs.common import clean_string, FileSystem
7+
from niemafs.common import clean_string, FileSystem, safename
88
from niemafs.gcm import GcmFS
99

1010
# imports
11+
from Crypto.Cipher import AES
1112
from datetime import datetime
13+
from multiprocessing import Pool
1214
from pathlib import Path
1315
from struct import unpack
1416
from warnings import warn
@@ -30,6 +32,12 @@
3032
4: 'Korea',
3133
}
3234

35+
# helper function for multiprocessing of AES decryption
36+
def aes_helper(aes_tup):
37+
decrypt_key, cluster_data_encrypted = aes_tup
38+
aes = AES.new(decrypt_key, AES.MODE_CBC, cluster_data_encrypted[0x3D0 : 0x3E0])
39+
return aes.decrypt(cluster_data_encrypted[0x0400:])
40+
3341
class WiiFS(FileSystem):
3442
'''Class to represent a `Nintendo Wii DVD <https://wiibrew.org/wiki/Wii_disc#%22System_Area%22>`_.'''
3543
def __init__(self, file_obj, path=None):
@@ -272,9 +280,6 @@ def parse_ticket(data):
272280
return out
273281

274282
def __iter__(self):
275-
# import PyCryptodome within WiiFS to allow people to use the other NiemaFS classes without it
276-
from Crypto.Cipher import AES
277-
278283
# parse each partition table
279284
for volume_num, parsed_partition_table in enumerate(self.parse_partition_tables()):
280285
volume_path = Path('Volume %d' % volume_num)
@@ -317,19 +322,16 @@ def __iter__(self):
317322
data_start_offset = partition['offset'] + partition_data_offset
318323
data_end_offset = data_start_offset + partition_data_size
319324
if WiiFS.parse_header(self.get_header())['disable_disc_encryption'] == 0:
320-
data_decrypted = b''
321-
for cluster_offset in range(data_start_offset, data_end_offset, 0x8000):
322-
cluster_data_encrypted = self.read_file(cluster_offset, 0x8000)
323-
aes = AES.new(decrypt_key, AES.MODE_CBC, cluster_data_encrypted[0x3D0 : 0x3E0])
324-
data_decrypted += aes.decrypt(cluster_data_encrypted[0x0400:])
325+
with Pool(processes=None) as pool:
326+
data_decrypted = b''.join(pool.map(aes_helper, [(decrypt_key, self.read_file(cluster_offset, 0x8000)) for cluster_offset in range(data_start_offset, data_end_offset, 0x8000)]))
325327
else:
326328
data_decrypted = self.read_file(data_start_offset, partition_data_size)
327329

328330
# yield partition header data
329331
partition_header = WiiFS.parse_header(data_decrypted[0x0000 : 0x0420])
330-
partition_path = volume_path / ('Partition %d (%s) (%s%s) (%s)' % (partition_num, partition_type.capitalize(), partition_header['game_code'], partition_header['maker_code'], partition_header['game_name']))
332+
partition_path = volume_path / ('Partition %d (%s) (%s%s) (%s)' % (partition_num, partition_type.capitalize(), partition_header['game_code'], partition_header['maker_code'], safename(partition_header['game_name'])))
331333
yield (partition_path, None, None)
332-
partition_header_path = partition_path / '__PARTITION_HEADER_NIEMAFS'
334+
partition_header_path = partition_path / '___PARTITION_HEADER_NIEMAFS'
333335
yield (partition_header_path, None, None)
334336
yield (partition_header_path / 'ticket.bin', None, ticket)
335337
yield (partition_header_path / 'tmd.bin', None, tmd)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
here = path.abspath(path.dirname(__file__))
1515
setup(
1616
name='niemafs', # Required
17-
version='0.0.9', # Required
17+
version='0.0.10', # Required
1818
description="Niema's Python library for reading data from various file system standards", # Required
1919
long_description="Niema's Python library for reading data from various file system standards", # Optional
2020
long_description_content_type='text/plain', # Optional (see note above)

0 commit comments

Comments
 (0)