nstools 1.2.2__tar.gz → 2.0.0b2__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.
- {nstools-1.2.2 → nstools-2.0.0b2}/PKG-INFO +3 -3
- {nstools-1.2.2 → nstools-2.0.0b2}/README.md +2 -2
- nstools-2.0.0b2/ns_verify_folder.py +110 -0
- {nstools-1.2.2/nstools/lib → nstools-2.0.0b2/nstools}/FsCert.py +2 -2
- nstools-2.0.0b2/nstools/FsNcz.py +74 -0
- {nstools-1.2.2/nstools/lib → nstools-2.0.0b2/nstools}/FsTools.py +3 -3
- {nstools-1.2.2/nstools/lib → nstools-2.0.0b2/nstools}/PathTools.py +0 -1
- {nstools-1.2.2/nstools/lib → nstools-2.0.0b2/nstools}/Verify.py +19 -23
- {nstools-1.2.2/nstools/lib → nstools-2.0.0b2/nstools}/VerifyTools.py +10 -14
- {nstools-1.2.2 → nstools-2.0.0b2}/nstools.egg-info/PKG-INFO +3 -3
- nstools-2.0.0b2/nstools.egg-info/SOURCES.txt +21 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/setup.py +3 -4
- nstools-1.2.2/ns_verify_folder.py +0 -156
- nstools-1.2.2/nstools/Fs/BaseFs.py +0 -178
- nstools-1.2.2/nstools/Fs/Bktr.py +0 -268
- nstools-1.2.2/nstools/Fs/Cnmt.py +0 -80
- nstools-1.2.2/nstools/Fs/File.py +0 -500
- nstools-1.2.2/nstools/Fs/Hfs0.py +0 -181
- nstools-1.2.2/nstools/Fs/Ivfc.py +0 -49
- nstools-1.2.2/nstools/Fs/Nacp.py +0 -613
- nstools-1.2.2/nstools/Fs/Nca.py +0 -305
- nstools-1.2.2/nstools/Fs/Nsp.py +0 -452
- nstools-1.2.2/nstools/Fs/Pfs0.py +0 -305
- nstools-1.2.2/nstools/Fs/Rom.py +0 -57
- nstools-1.2.2/nstools/Fs/Ticket.py +0 -226
- nstools-1.2.2/nstools/Fs/Type.py +0 -30
- nstools-1.2.2/nstools/Fs/Xci.py +0 -355
- nstools-1.2.2/nstools/Fs/__init__.py +0 -27
- nstools-1.2.2/nstools/lib/BlockDecompressorReader.py +0 -65
- nstools-1.2.2/nstools/lib/FsNcaMod.py +0 -255
- nstools-1.2.2/nstools/lib/Header.py +0 -27
- nstools-1.2.2/nstools/nut/Hex.py +0 -40
- nstools-1.2.2/nstools/nut/Keys.py +0 -202
- nstools-1.2.2/nstools/nut/Print.py +0 -45
- nstools-1.2.2/nstools/nut/Titles.py +0 -78
- nstools-1.2.2/nstools/nut/aes128.py +0 -428
- nstools-1.2.2/nstools.egg-info/SOURCES.txt +0 -43
- {nstools-1.2.2 → nstools-2.0.0b2}/LICENSE.md +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/bin/ns-verify-folder +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/bin/ns-verify-folder-log +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/bin/ns-verify-folder-log.bat +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/bin/ns-verify-folder.bat +0 -0
- {nstools-1.2.2/nstools/lib → nstools-2.0.0b2/nstools}/NcaKeys.py +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/nstools.egg-info/dependency_links.txt +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/nstools.egg-info/requires.txt +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/nstools.egg-info/top_level.txt +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/nstools.egg-info/zip-safe +0 -0
- {nstools-1.2.2 → nstools-2.0.0b2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nstools
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b2
|
|
4
4
|
Home-page: https://github.com/seiya-dev/NSTools
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -20,11 +20,11 @@ Dynamic: requires-python
|
|
|
20
20
|
|
|
21
21
|
# Nintendo Switch Tools
|
|
22
22
|
|
|
23
|
-
Tools for
|
|
23
|
+
Tools for XCI, XCZ, NSP and NSZ
|
|
24
24
|
|
|
25
25
|
Based on nut, NSC_B and nsz
|
|
26
26
|
|
|
27
27
|
# pypi.org
|
|
28
28
|
|
|
29
|
-
for using nstools
|
|
29
|
+
for using nstools, nsz.Fs and nsz.nut:
|
|
30
30
|
https://pypi.org/project/nstools/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Nintendo Switch Tools
|
|
2
2
|
|
|
3
|
-
Tools for
|
|
3
|
+
Tools for XCI, XCZ, NSP and NSZ
|
|
4
4
|
|
|
5
5
|
Based on nut, NSC_B and nsz
|
|
6
6
|
|
|
7
7
|
# pypi.org
|
|
8
8
|
|
|
9
|
-
for using nstools
|
|
9
|
+
for using nstools, nsz.Fs and nsz.nut:
|
|
10
10
|
https://pypi.org/project/nstools/
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#! /usr/bin/python3
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import argparse
|
|
5
|
+
import requests
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from nsz.nut import Keys
|
|
10
|
+
from nstools import Verify
|
|
11
|
+
|
|
12
|
+
# set app path
|
|
13
|
+
appPath = Path(sys.argv[0])
|
|
14
|
+
while not appPath.is_dir():
|
|
15
|
+
appPath = appPath.parents[0]
|
|
16
|
+
appPath = Path(appPath).resolve().as_posix()
|
|
17
|
+
print(f'[:INFO:] App Path: {appPath}')
|
|
18
|
+
|
|
19
|
+
# set args
|
|
20
|
+
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
|
|
21
|
+
parser.add_argument('-i', '--input', help = 'input folder')
|
|
22
|
+
parser.add_argument('-w', '--webhook-url', help = 'discord webhook url', required = False)
|
|
23
|
+
parser.add_argument('--save-log', help = 'save verify log', required = False, action='store_true')
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
INCP_PATH = args.input
|
|
27
|
+
WHOOK_URL = args.webhook_url
|
|
28
|
+
SAVE_VLOG = bool(args.save_log)
|
|
29
|
+
|
|
30
|
+
Keys.load_default()
|
|
31
|
+
if not Keys.keys_loaded:
|
|
32
|
+
input('Press Enter to exit...')
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
def send_hook(message_content: str = '', PadPrint: bool = False):
|
|
36
|
+
if message_content == '':
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
print_msg = message_content
|
|
40
|
+
if PadPrint == True:
|
|
41
|
+
print_msg = f'\n{message_content}'
|
|
42
|
+
print(print_msg)
|
|
43
|
+
payload = {
|
|
44
|
+
'username': 'Contributions',
|
|
45
|
+
'content': message_content.strip()
|
|
46
|
+
}
|
|
47
|
+
headers = {"Content-type": "application/json"}
|
|
48
|
+
response = requests.post(WHOOK_URL, data=json.dumps(payload), headers=headers)
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
except:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def scan_folder():
|
|
54
|
+
ipath = Path(INCP_PATH).resolve().as_posix()
|
|
55
|
+
|
|
56
|
+
if not Path(ipath).exists():
|
|
57
|
+
print(f'[:WARN:] Please put your files in "{ipath}" and run this script again.')
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
files = list()
|
|
61
|
+
|
|
62
|
+
for item in sorted(list(Path(ipath).iterdir())):
|
|
63
|
+
item_path = Path(item)
|
|
64
|
+
if not item_path.is_file() or item_path.is_symlink():
|
|
65
|
+
continue
|
|
66
|
+
if not item_path.name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
|
|
67
|
+
continue
|
|
68
|
+
files.append(item.as_posix())
|
|
69
|
+
|
|
70
|
+
findex = 0
|
|
71
|
+
for item in sorted(files):
|
|
72
|
+
item_path = Path(item)
|
|
73
|
+
|
|
74
|
+
findex += 1
|
|
75
|
+
send_hook(f'[:INFO:] File found ({findex} of {len(files)}): {item_path.name}', True)
|
|
76
|
+
send_hook(f'[:INFO:] Checking filename...')
|
|
77
|
+
|
|
78
|
+
data = Verify.parse_name(item_path.name)
|
|
79
|
+
|
|
80
|
+
if data is None:
|
|
81
|
+
send_hook(f'{item_path.name}: BAD NAME')
|
|
82
|
+
|
|
83
|
+
rootpath = item_path.parent.as_posix()
|
|
84
|
+
basename = item_path.name
|
|
85
|
+
basename = f'{basename[:-4]}-{basename[-3:]}-verify'
|
|
86
|
+
log_name = Path(rootpath).joinpath(basename).as_posix()
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
send_hook(f'[:INFO:] Verifying...')
|
|
90
|
+
nspTest, nspLog = Verify.verify(item_path.as_posix())
|
|
91
|
+
if nspTest != True:
|
|
92
|
+
send_hook(f'{item_path}: BAD', True)
|
|
93
|
+
else:
|
|
94
|
+
send_hook(f'{item_path}: OK', True)
|
|
95
|
+
if SAVE_VLOG == True:
|
|
96
|
+
if nspTest != True:
|
|
97
|
+
with open(f'{log_name}-bad.log', 'w') as f:
|
|
98
|
+
f.write(f'{nspLog}')
|
|
99
|
+
else:
|
|
100
|
+
with open(f'{log_name}-ok.log', 'w') as f:
|
|
101
|
+
f.write(f'{nspLog}')
|
|
102
|
+
except Exception as e:
|
|
103
|
+
send_hook(f'[:WARN:] An error occurred:\n{item_path}: {str(e)}')
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
if INCP_PATH:
|
|
107
|
+
scan_folder()
|
|
108
|
+
else:
|
|
109
|
+
parser.print_help()
|
|
110
|
+
print()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from binascii import a2b_base64
|
|
2
2
|
|
|
3
3
|
class PublicCert:
|
|
4
4
|
def getPublic(ctype = None):
|
|
@@ -86,5 +86,5 @@ class PublicCert:
|
|
|
86
86
|
}
|
|
87
87
|
if ctype not in certlist:
|
|
88
88
|
ctype = 'Tinfoil'
|
|
89
|
-
chain = bytearray(
|
|
89
|
+
chain = bytearray(a2b_base64(certlist[ctype]))
|
|
90
90
|
return chain
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from binascii import hexlify as hx, unhexlify as uhx
|
|
2
|
+
|
|
3
|
+
from nsz.nut import Keys
|
|
4
|
+
from nsz.nut import Print
|
|
5
|
+
|
|
6
|
+
from nsz.Fs import Type
|
|
7
|
+
from nsz.Fs.File import File
|
|
8
|
+
from nsz.Fs.Nca import NcaHeader as NczHeader
|
|
9
|
+
|
|
10
|
+
class Ncz(File):
|
|
11
|
+
def __init__(self, path = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1):
|
|
12
|
+
self.header = None
|
|
13
|
+
self.sectionFilesystems = []
|
|
14
|
+
self.sections = []
|
|
15
|
+
super(Ncz, self).__init__(path, mode, cryptoType, cryptoKey, cryptoCounter)
|
|
16
|
+
|
|
17
|
+
def __iter__(self):
|
|
18
|
+
return self.sectionFilesystems.__iter__()
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key):
|
|
21
|
+
return self.sectionFilesystems[key]
|
|
22
|
+
|
|
23
|
+
def open(self, file = None, mode = 'rb', cryptoType = -1, cryptoKey = -1, cryptoCounter = -1, meta_only=False):
|
|
24
|
+
super(Ncz, self).open(file, mode, cryptoType, cryptoKey, cryptoCounter, meta_only)
|
|
25
|
+
|
|
26
|
+
self.header = NczHeader()
|
|
27
|
+
self.partition(0x0, 0xC00, self.header, Type.Crypto.XTS, uhx(Keys.get('header_key')))
|
|
28
|
+
self.header.seek(0x400)
|
|
29
|
+
|
|
30
|
+
def masterKey(self):
|
|
31
|
+
return max(self.header.cryptoType, self.header.cryptoType2)
|
|
32
|
+
|
|
33
|
+
def buildId(self):
|
|
34
|
+
if self.header.contentType != Type.Content.PROGRAM:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
f = self[0]['main']
|
|
39
|
+
f.seek(0x40)
|
|
40
|
+
return hx(f.read(0x20)).decode('utf8').upper()
|
|
41
|
+
except IOError as e:
|
|
42
|
+
pass
|
|
43
|
+
except:
|
|
44
|
+
raise
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def printInfo(self, maxDepth = 3, indent = 0):
|
|
48
|
+
tabs = '\t' * indent
|
|
49
|
+
Print.info('\n%sNCA Archive\n' % (tabs))
|
|
50
|
+
super(Ncz, self).printInfo(maxDepth, indent)
|
|
51
|
+
|
|
52
|
+
Print.info(tabs + 'magic = ' + str(self.header.magic))
|
|
53
|
+
Print.info(tabs + 'titleId = ' + str(self.header.titleId))
|
|
54
|
+
Print.info(tabs + 'rightsId = ' + str(self.header.rightsId))
|
|
55
|
+
Print.info(tabs + 'isGameCard = ' + hex(self.header.isGameCard))
|
|
56
|
+
Print.info(tabs + 'contentType = ' + str(self.header.contentType))
|
|
57
|
+
Print.info(tabs + 'cryptoType = ' + str(self.cryptoType))
|
|
58
|
+
Print.info(tabs + 'Size: ' + str(self.header.size))
|
|
59
|
+
Print.info(tabs + 'crypto master key: ' + str(self.header.cryptoType))
|
|
60
|
+
Print.info(tabs + 'crypto master key2: ' + str(self.header.cryptoType2))
|
|
61
|
+
Print.info(tabs + 'key Index: ' + str(self.header.keyIndex))
|
|
62
|
+
#Print.info(tabs + 'key Block: ' + str(self.header.getKeyBlock()))
|
|
63
|
+
for key in self.header.keys:
|
|
64
|
+
if key:
|
|
65
|
+
Print.info(tabs + 'key Block: ' + str(hx(key)))
|
|
66
|
+
|
|
67
|
+
if(indent+1 < maxDepth):
|
|
68
|
+
Print.info('\n%sPartitions:' % (tabs))
|
|
69
|
+
|
|
70
|
+
for s in self:
|
|
71
|
+
s.printInfo(maxDepth, indent+1)
|
|
72
|
+
|
|
73
|
+
if self.header.contentType == Type.Content.PROGRAM:
|
|
74
|
+
Print.info(tabs + 'build Id: ' + str(self.buildId()))
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from binascii import hexlify as hx, unhexlify as uhx
|
|
2
2
|
from hashlib import sha256, sha1
|
|
3
|
-
|
|
4
3
|
from copy import copy
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
from .FsNcz import Ncz
|
|
6
6
|
|
|
7
7
|
def get_ncz_data(src_nca):
|
|
8
8
|
nca = copy(src_nca)
|
|
9
|
-
nca =
|
|
9
|
+
nca = Ncz(nca)
|
|
10
10
|
return nca
|
|
11
11
|
|
|
12
12
|
def get_data_from_cnmt(nca):
|
|
@@ -1,29 +1,25 @@
|
|
|
1
|
+
from os.path import basename as fsBasename, abspath as fsAbsPath
|
|
1
2
|
from binascii import hexlify as hx, unhexlify as uhx
|
|
2
|
-
from
|
|
3
|
+
from re import search as re_search
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from pathlib import Path
|
|
3
6
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import re
|
|
7
|
+
from zstandard import ZstdDecompressor
|
|
8
|
+
from enlighten import Counter as pb_Counter
|
|
7
9
|
|
|
8
|
-
from
|
|
10
|
+
from nsz import Header, BlockDecompressorReader
|
|
11
|
+
from nsz.Fs import factory
|
|
12
|
+
from nsz.Fs import Xci, Nsp
|
|
13
|
+
from nsz.Fs import Nca, Ticket
|
|
14
|
+
from nsz.Fs import Type
|
|
9
15
|
|
|
10
16
|
from . import FsTools
|
|
11
17
|
from . import VerifyTools
|
|
12
|
-
from . import Header, BlockDecompressorReader
|
|
13
18
|
from .FsCert import PublicCert
|
|
14
19
|
|
|
15
|
-
import zstandard
|
|
16
|
-
import enlighten
|
|
17
|
-
|
|
18
|
-
from nstools.Fs import factory
|
|
19
|
-
from nstools.Fs import Xci, Nsp
|
|
20
|
-
from nstools.Fs import Nca, Ticket
|
|
21
|
-
from nstools.Fs import Type
|
|
22
|
-
|
|
23
|
-
|
|
24
20
|
def parse_name(file: str):
|
|
25
|
-
res_id =
|
|
26
|
-
res_ver =
|
|
21
|
+
res_id = re_search(r'(?P<title_id>\[0100[A-F0-9]{12}\])', file)
|
|
22
|
+
res_ver = re_search(r'(?P<version>\[v\d+\])', file)
|
|
27
23
|
|
|
28
24
|
if res_id is None or res_ver is None:
|
|
29
25
|
return None
|
|
@@ -68,7 +64,7 @@ def verify(file: str, vlevel: int = 3):
|
|
|
68
64
|
vlevel = 3
|
|
69
65
|
|
|
70
66
|
try:
|
|
71
|
-
filename =
|
|
67
|
+
filename = fsAbsPath(file)
|
|
72
68
|
|
|
73
69
|
check = True
|
|
74
70
|
vmsg = list()
|
|
@@ -100,7 +96,7 @@ def verify(file: str, vlevel: int = 3):
|
|
|
100
96
|
f.flush()
|
|
101
97
|
f.close()
|
|
102
98
|
|
|
103
|
-
outlog =
|
|
99
|
+
outlog = fsBasename(file) + '\n'
|
|
104
100
|
outlog += '\n'.join(vmsg) + '\n'
|
|
105
101
|
|
|
106
102
|
return check, outlog
|
|
@@ -192,7 +188,7 @@ def verify_decrypt(nspx, vmsg = None):
|
|
|
192
188
|
vmsg.append(tvmsg)
|
|
193
189
|
if f.header.contentType != Type.Content.PROGRAM:
|
|
194
190
|
correct = VerifyTools.verify_enforcer(f)
|
|
195
|
-
if correct == True and f.header.contentType == Type.Content.
|
|
191
|
+
if correct == True and f.header.contentType == Type.Content.PUBLICDATA and f.header.getRightsId() == 0:
|
|
196
192
|
correct = VerifyTools.pr_noenc_check_dlc(f)
|
|
197
193
|
if correct == False:
|
|
198
194
|
bad_dec = True
|
|
@@ -571,7 +567,7 @@ def verify_hash(nspx, headerlist, vmsg = None):
|
|
|
571
567
|
counter = 0
|
|
572
568
|
mbDiv = 1048576
|
|
573
569
|
BAR_FMT = u'{desc}{desc_pad}{percentage:3.0f}%|{bar}| {count:{len_total}d}/{total:d} {unit} [{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]'
|
|
574
|
-
bar =
|
|
570
|
+
bar = pb_Counter(total = nca_size//mbDiv, desc='Hashing', unit='MiB', color='red', bar_format=BAR_FMT)
|
|
575
571
|
|
|
576
572
|
i = 0
|
|
577
573
|
f.rewind();
|
|
@@ -651,7 +647,7 @@ def verify_hash(nspx, headerlist, vmsg = None):
|
|
|
651
647
|
counter = 0
|
|
652
648
|
mbDiv = 1048576
|
|
653
649
|
BAR_FMT = u'{desc}{desc_pad}{percentage:3.0f}%|{bar}| {count:{len_total}d}/{total:d} {unit} [{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]'
|
|
654
|
-
bar =
|
|
650
|
+
bar = pb_Counter(total = nca_size//mbDiv, desc='Hashing', unit='MiB', color='red', bar_format=BAR_FMT)
|
|
655
651
|
|
|
656
652
|
i = 0
|
|
657
653
|
f.rewind();
|
|
@@ -704,7 +700,7 @@ def verify_hash(nspx, headerlist, vmsg = None):
|
|
|
704
700
|
pos = f.tell()
|
|
705
701
|
|
|
706
702
|
if not useBlockCompression:
|
|
707
|
-
decompressor =
|
|
703
|
+
decompressor = ZstdDecompressor().stream_reader(f)
|
|
708
704
|
|
|
709
705
|
spsize = 0
|
|
710
706
|
|
|
@@ -7,24 +7,20 @@ from Crypto.Util import Counter
|
|
|
7
7
|
from Crypto.PublicKey import RSA
|
|
8
8
|
from Crypto.Signature import PKCS1_v1_5, PKCS1_PSS
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
from zstandard import ZstdDecompressor
|
|
11
11
|
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
12
|
+
from nsz import Header, BlockDecompressorReader
|
|
13
|
+
from nsz.nut import Hex
|
|
14
|
+
from nsz.nut import Keys
|
|
15
|
+
from nsz.nut import aes128
|
|
16
|
+
from nsz.Fs import Type
|
|
17
|
+
from nsz.Fs import File
|
|
18
|
+
from nsz.Fs import Nca
|
|
19
|
+
from nsz.Fs import Ticket
|
|
15
20
|
|
|
16
21
|
from . import FsTools
|
|
17
|
-
from . import Header, BlockDecompressorReader
|
|
18
22
|
from .NcaKeys import getNcaModulusKey
|
|
19
23
|
|
|
20
|
-
import zstandard
|
|
21
|
-
|
|
22
|
-
from nstools.Fs import Type
|
|
23
|
-
from nstools.Fs import File
|
|
24
|
-
from nstools.Fs import Nca
|
|
25
|
-
from nstools.Fs import Ticket
|
|
26
|
-
|
|
27
|
-
|
|
28
24
|
RSA_PUBLIC_EXPONENT = 0x10001
|
|
29
25
|
FS_HEADER_LENGTH = 0x200
|
|
30
26
|
UNCOMPRESSABLE_HEADER_SIZE = 0x4000
|
|
@@ -136,7 +132,7 @@ def verify_ncz(self, target):
|
|
|
136
132
|
pos = f.tell()
|
|
137
133
|
|
|
138
134
|
if not useBlockCompression:
|
|
139
|
-
decompressor =
|
|
135
|
+
decompressor = ZstdDecompressor().stream_reader(f)
|
|
140
136
|
|
|
141
137
|
count = 0
|
|
142
138
|
checkstarter = 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nstools
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0b2
|
|
4
4
|
Home-page: https://github.com/seiya-dev/NSTools
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -20,11 +20,11 @@ Dynamic: requires-python
|
|
|
20
20
|
|
|
21
21
|
# Nintendo Switch Tools
|
|
22
22
|
|
|
23
|
-
Tools for
|
|
23
|
+
Tools for XCI, XCZ, NSP and NSZ
|
|
24
24
|
|
|
25
25
|
Based on nut, NSC_B and nsz
|
|
26
26
|
|
|
27
27
|
# pypi.org
|
|
28
28
|
|
|
29
|
-
for using nstools
|
|
29
|
+
for using nstools, nsz.Fs and nsz.nut:
|
|
30
30
|
https://pypi.org/project/nstools/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
README.md
|
|
3
|
+
ns_verify_folder.py
|
|
4
|
+
setup.py
|
|
5
|
+
bin/ns-verify-folder
|
|
6
|
+
bin/ns-verify-folder-log
|
|
7
|
+
bin/ns-verify-folder-log.bat
|
|
8
|
+
bin/ns-verify-folder.bat
|
|
9
|
+
nstools/FsCert.py
|
|
10
|
+
nstools/FsNcz.py
|
|
11
|
+
nstools/FsTools.py
|
|
12
|
+
nstools/NcaKeys.py
|
|
13
|
+
nstools/PathTools.py
|
|
14
|
+
nstools/Verify.py
|
|
15
|
+
nstools/VerifyTools.py
|
|
16
|
+
nstools.egg-info/PKG-INFO
|
|
17
|
+
nstools.egg-info/SOURCES.txt
|
|
18
|
+
nstools.egg-info/dependency_links.txt
|
|
19
|
+
nstools.egg-info/requires.txt
|
|
20
|
+
nstools.egg-info/top_level.txt
|
|
21
|
+
nstools.egg-info/zip-safe
|
|
@@ -16,7 +16,7 @@ if readmePath.is_file():
|
|
|
16
16
|
|
|
17
17
|
setuptools.setup(
|
|
18
18
|
name = 'nstools',
|
|
19
|
-
version = '
|
|
19
|
+
version = '2.0.0b2',
|
|
20
20
|
url = 'https://github.com/seiya-dev/NSTools',
|
|
21
21
|
long_description = long_description,
|
|
22
22
|
long_description_content_type = 'text/markdown',
|
|
@@ -31,11 +31,10 @@ setuptools.setup(
|
|
|
31
31
|
],
|
|
32
32
|
|
|
33
33
|
packages = [
|
|
34
|
-
'nstools
|
|
35
|
-
'nstools.nut',
|
|
36
|
-
'nstools.lib',
|
|
34
|
+
'nstools',
|
|
37
35
|
],
|
|
38
36
|
install_requires = [
|
|
37
|
+
# 'nsz @ git+https://github.com/nicoboss/nsz.git@...', # use requirements.txt
|
|
39
38
|
'zstandard',
|
|
40
39
|
'enlighten',
|
|
41
40
|
'requests',
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
#! /usr/bin/python3
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import json
|
|
6
|
-
import requests
|
|
7
|
-
import re
|
|
8
|
-
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from nstools.nut import Keys
|
|
12
|
-
|
|
13
|
-
from nstools.lib import Verify
|
|
14
|
-
|
|
15
|
-
# set app path
|
|
16
|
-
appPath = Path(sys.argv[0])
|
|
17
|
-
while not appPath.is_dir():
|
|
18
|
-
appPath = appPath.parents[0]
|
|
19
|
-
appPath = os.path.abspath(appPath)
|
|
20
|
-
print(f'[:INFO:] App Path: {appPath}')
|
|
21
|
-
|
|
22
|
-
# set logs path
|
|
23
|
-
# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs'))
|
|
24
|
-
# print(f'[:INFO:] Logs Path: {logs_dir}')
|
|
25
|
-
|
|
26
|
-
import argparse
|
|
27
|
-
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
|
|
28
|
-
parser.add_argument('-i', '--input', help = 'input folder')
|
|
29
|
-
parser.add_argument('-w', '--webhook-url', help = 'discord webhook url', required = False)
|
|
30
|
-
parser.add_argument('--save-log', help = 'save verify log', required = False, action='store_true')
|
|
31
|
-
args = parser.parse_args()
|
|
32
|
-
|
|
33
|
-
INCP_PATH = args.input
|
|
34
|
-
WHOOK_URL = args.webhook_url
|
|
35
|
-
SAVE_VLOG = bool(args.save_log)
|
|
36
|
-
|
|
37
|
-
Keys.load_default()
|
|
38
|
-
if not Keys.keys_loaded:
|
|
39
|
-
input('Press Enter to exit...')
|
|
40
|
-
sys.exit(1)
|
|
41
|
-
|
|
42
|
-
def send_hook(message_content: str = '', PadPrint: bool = False):
|
|
43
|
-
if message_content == '':
|
|
44
|
-
return
|
|
45
|
-
try:
|
|
46
|
-
print_msg = message_content
|
|
47
|
-
if PadPrint == True:
|
|
48
|
-
print_msg = f'\n{message_content}'
|
|
49
|
-
print(print_msg)
|
|
50
|
-
payload = {
|
|
51
|
-
'username': 'Contributions',
|
|
52
|
-
'content': message_content.strip()
|
|
53
|
-
}
|
|
54
|
-
headers = {"Content-type": "application/json"}
|
|
55
|
-
response = requests.post(WHOOK_URL, data=json.dumps(payload), headers=headers)
|
|
56
|
-
response.raise_for_status()
|
|
57
|
-
except:
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def scan_folder():
|
|
61
|
-
ipath = os.path.abspath(INCP_PATH)
|
|
62
|
-
fname = os.path.basename(ipath).upper()
|
|
63
|
-
|
|
64
|
-
# lpath_badfolder = os.path.join(logs_dir, 'bad-folder.log')
|
|
65
|
-
# lpath_badname = os.path.join(logs_dir, 'bad-names.log')
|
|
66
|
-
# lpath_badfile = os.path.join(logs_dir, 'bad-file.log')
|
|
67
|
-
|
|
68
|
-
# if not os.path.exists(logs_dir):
|
|
69
|
-
# os.makedirs(logs_dir)
|
|
70
|
-
|
|
71
|
-
# if os.path.exists(lpath_badfolder):
|
|
72
|
-
# os.remove(lpath_badfolder)
|
|
73
|
-
# if os.path.exists(lpath_badname):
|
|
74
|
-
# os.remove(lpath_badname)
|
|
75
|
-
# if os.path.exists(lpath_badfile):
|
|
76
|
-
# os.remove(lpath_badfile)
|
|
77
|
-
|
|
78
|
-
if not os.path.exists(ipath):
|
|
79
|
-
print(f'[:WARN:] Please put your files in "{ipath}" and run this script again.')
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
files = list()
|
|
83
|
-
for item in sorted(os.listdir(ipath)):
|
|
84
|
-
item_path = os.path.join(ipath, item)
|
|
85
|
-
if not os.path.isfile(item_path):
|
|
86
|
-
continue
|
|
87
|
-
if not item.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
|
|
88
|
-
continue
|
|
89
|
-
files.append(item)
|
|
90
|
-
|
|
91
|
-
findex = 0
|
|
92
|
-
for item in sorted(files):
|
|
93
|
-
item_path = os.path.join(ipath, item)
|
|
94
|
-
|
|
95
|
-
findex += 1
|
|
96
|
-
send_hook(f'[:INFO:] File found ({findex} of {len(files)}): {item_path}', True)
|
|
97
|
-
send_hook(f'[:INFO:] Checking filename...')
|
|
98
|
-
|
|
99
|
-
data = Verify.parse_name(item)
|
|
100
|
-
|
|
101
|
-
if data is None:
|
|
102
|
-
send_hook(f'{item_path}: BAD NAME')
|
|
103
|
-
# with open(lpath_badname, 'a') as f:
|
|
104
|
-
# f.write(f'{item_path}\n')
|
|
105
|
-
|
|
106
|
-
# if data is not None and re.match(r'^BASE|UPD(ATE)?|DLC|XCI$', fname) is not None:
|
|
107
|
-
# if item.lower().endswith(('.xci', '.xcz')):
|
|
108
|
-
# iscart = True
|
|
109
|
-
# else:
|
|
110
|
-
# iscart = False
|
|
111
|
-
# if fname == 'UPDATE':
|
|
112
|
-
# fname = 'UPD'
|
|
113
|
-
# if fname == 'BASE' and data['title_type'] != 'BASE' or fname == 'BASE' and iscart == True:
|
|
114
|
-
# with open(lpath_badfolder, 'a') as f:
|
|
115
|
-
# f.write(f'{item_path}\n')
|
|
116
|
-
# if fname == 'UPD' and data['title_type'] != 'UPD' or fname == 'UPD' and iscart == True:
|
|
117
|
-
# with open(lpath_badfolder, 'a') as f:
|
|
118
|
-
# f.write(f'{item_path}\n')
|
|
119
|
-
# if fname == 'DLC' and data['title_type'] != 'DLC' or fname == 'DLC' and iscart == True:
|
|
120
|
-
# with open(lpath_badfolder, 'a') as f:
|
|
121
|
-
# f.write(f'{item_path}\n')
|
|
122
|
-
# if fname == 'XCI' and iscart == False:
|
|
123
|
-
# with open(lpath_badfolder, 'a') as f:
|
|
124
|
-
# f.write(f'{item_path}\n')
|
|
125
|
-
|
|
126
|
-
rootpath = os.path.dirname(item_path)
|
|
127
|
-
basename = os.path.basename(item_path)
|
|
128
|
-
basename = f'{basename[:-4]}-{basename[-3:]}-verify'
|
|
129
|
-
log_name = os.path.join(rootpath, basename)
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
send_hook(f'[:INFO:] Verifying...')
|
|
133
|
-
nspTest, nspLog = Verify.verify(item_path)
|
|
134
|
-
if nspTest != True:
|
|
135
|
-
send_hook(f'{item_path}: BAD', True)
|
|
136
|
-
# with open(lpath_badfile, 'a') as f:
|
|
137
|
-
# f.write(f'{item_path}\n')
|
|
138
|
-
else:
|
|
139
|
-
send_hook(f'{item_path}: OK', True)
|
|
140
|
-
if SAVE_VLOG == True:
|
|
141
|
-
if nspTest != True:
|
|
142
|
-
with open(f'{log_name}-bad.log', 'w') as f:
|
|
143
|
-
f.write(f'{nspLog}')
|
|
144
|
-
else:
|
|
145
|
-
with open(f'{log_name}-ok.log', 'w') as f:
|
|
146
|
-
f.write(f'{nspLog}')
|
|
147
|
-
except Exception as e:
|
|
148
|
-
send_hook(f'[:WARN:] An error occurred:\n{item_path}: {str(e)}')
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if __name__ == "__main__":
|
|
152
|
-
if INCP_PATH:
|
|
153
|
-
scan_folder()
|
|
154
|
-
else:
|
|
155
|
-
parser.print_help()
|
|
156
|
-
print()
|