Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2ad465f
use nsz package for nut and Fs
seiya-git Jun 5, 2026
c9a77fb
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Jun 5, 2026
b7fe7bc
Replace BlockDecompressorReader implementation with nsz import
seiya-git Jun 5, 2026
4d5b474
Update nsz commit and use minimum versions for requests and pycryptodome
seiya-git Jun 8, 2026
7b71baa
Remove local Header and BlockDecompressorReader imports, use nsz package
seiya-git Jun 8, 2026
9a5eaf6
Refactor imports to prefer nsz modules and optimize standard lib usage
seiya-git Jun 8, 2026
c5c1225
Remove unused file write and sha1 import from verification logic
seiya-git Jun 8, 2026
7f50d0b
Refactored imports and unified app path handling across scripts
seiya-git Jun 8, 2026
d4384bf
Replace req_post with requests.post in send_hook function
seiya-git Jun 8, 2026
6ae6b26
Refactor file path handling with pathlib and improve input file checks
seiya-git Jun 8, 2026
e527c3f
Remove unnecessary 'rb' mode in container.open calls across files
seiya-git Jun 8, 2026
9b834bf
Move SectionTableEntry and NcaHeader imports to nsz.Fs.Nca module
seiya-git Jun 8, 2026
a79ae2c
Remove unused imports 'pack', 'unpack', 'sha256', and 'Titles'
seiya-git Jun 8, 2026
98c0da7
Convert Nca class methods to use spaces for indentation instead of tabs
seiya-git Jun 8, 2026
e766d05
Cleaned up whitespace and indentation in Nca class methods
seiya-git Jun 8, 2026
900f76c
Replace FsNcaMod with FsNcz and update references to Ncz class
seiya-git Jun 8, 2026
8af7318
replace Fs and nut to nsz.Fs and nsz.nut
seiya-git Jun 8, 2026
87ae6e9
remove nstools.Fs and nstools.nut
seiya-git Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Nintendo Switch Tools

Tools for XCI, XCZ, NSP and NSZ
Tools for XCI, XCZ, NSP and NSZ

Based on nut, NSC_B and nsz

# pypi.org

for using nstools.Fs, nstools.lib and nstools.nut:
for using nstools, nsz.Fs and nsz.nut:
https://pypi.org/project/nstools/
7 changes: 3 additions & 4 deletions build/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

setuptools.setup(
name = 'nstools',
version = '1.2.3',
version = '2.0.0b1',
url = 'https://github.com/seiya-dev/NSTools',
long_description = long_description,
long_description_content_type = 'text/markdown',
Expand All @@ -31,11 +31,10 @@
],

packages = [
'nstools.Fs',
'nstools.nut',
'nstools.lib',
'nstools',
],
install_requires = [
'nsz @ git+https://github.com/nicoboss/nsz.git@2aac384',
'zstandard',
'enlighten',
'requests',
Expand Down
35 changes: 15 additions & 20 deletions py/ns_extract_hashes.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
#! /usr/bin/python3

from binascii import hexlify as hx, unhexlify as uhx

import os
import sys

from pathlib import Path
import argparse
import sys

from nstools.nut import Keys

from nstools.Fs import factory
from nstools.Fs import Pfs0, Nca, Type

from nstools.lib import FsTools
from nsz.nut import Keys
from nsz.Fs import factory
from nsz.Fs import Pfs0, Nca, Type

from nstools import FsTools

# set app path
appPath = Path(sys.argv[0])
while not appPath.is_dir():
appPath = appPath.parents[0]
appPath = os.path.abspath(appPath)
appPath = Path(appPath).resolve().as_posix()
print(f'[:INFO:] App Path: {appPath}')

# set logs path
# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs'))
# print(f'[:INFO:] Logs Path: {logs_dir}')

import argparse
# set args
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-i', '--input', help = 'input file')
args = parser.parse_args()
Expand All @@ -45,16 +37,19 @@ def send_hook(message_content):
pass

def scan_file():
ipath = os.path.abspath(INCP_PATH)
if not os.path.isfile(ipath):
ipath = Path(INCP_PATH).resolve().as_posix()

if not Path(ipath).is_file() or Path(ipath).is_symlink():
return
if not ipath.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
if not Path(ipath).name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
return

container = factory(Path(ipath).resolve())
container.open(ipath, 'rb')
container.open(ipath)

if ipath.lower().endswith(('.xci', '.xcz')):
container = container.hfs0['secure']

try:
for nspf in container:
if isinstance(nspf, Nca.Nca) and nspf.header.contentType == Type.Content.META:
Expand Down
34 changes: 14 additions & 20 deletions py/ns_extract_meta.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
#! /usr/bin/python3

from binascii import hexlify as hx, unhexlify as uhx

import os
import sys

from pathlib import Path
import argparse
import sys

from nstools.nut import Keys

from nstools.Fs import factory
from nstools.Fs import Pfs0, Xci, Nsp, Nca, Type

from nstools.lib import FsTools
from nsz.nut import Keys
from nsz.Fs import factory
from nsz.Fs import Pfs0, Xci, Nsp, Nca, Type

from nstools import FsTools

# set app path
appPath = Path(sys.argv[0])
while not appPath.is_dir():
appPath = appPath.parents[0]
appPath = os.path.abspath(appPath)
appPath = Path(appPath).resolve().as_posix()
print(f'[:INFO:] App Path: {appPath}')

# set logs path
# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs'))
# print(f'[:INFO:] Logs Path: {logs_dir}')

import argparse
# set args
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-i', '--input', help = 'input file')
args = parser.parse_args()
Expand Down Expand Up @@ -77,14 +69,16 @@ def extract_meta_from_cnmt(cnmt_sections):
print(f'> HASH: {entry.hash.hex()}')

def scan_file():
ipath = os.path.abspath(INCP_PATH)
if not os.path.isfile(ipath):
ipath = Path(INCP_PATH).resolve().as_posix()

if not Path(ipath).is_file() or Path(ipath).is_symlink():
return
if not ipath.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
if not Path(ipath).name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
return

container = factory(Path(ipath).resolve())
container.open(ipath, 'rb', meta_only=True)
container.open(ipath, meta_only=True)

try:
for cnmt in get_cnmts(container):
extract_meta_from_cnmt(cnmt)
Expand Down
30 changes: 13 additions & 17 deletions py/ns_ticket_info.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
#! /usr/bin/python3

import os
import sys

from pathlib import Path
import argparse
import sys

from nstools.nut import Keys

from nstools.Fs import factory, Ticket
from nsz.nut import Keys
from nsz.Fs import factory, Ticket

# set app path
appPath = Path(sys.argv[0])
while not appPath.is_dir():
appPath = appPath.parents[0]
appPath = os.path.abspath(appPath)
appPath = Path(appPath).resolve().as_posix()
print(f'[:INFO:] App Path: {appPath}')

# set logs path
# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs'))
# print(f'[:INFO:] Logs Path: {logs_dir}')

import argparse
# set args
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-i', '--input', help = 'input file')
args = parser.parse_args()
Expand All @@ -39,15 +33,17 @@ def send_hook(message_content):
pass

def scan_file():
ipath = os.path.abspath(INCP_PATH)
if not os.path.isfile(ipath):
ipath = Path(INCP_PATH).resolve().as_posix()

if not Path(ipath).is_file() or Path(ipath).is_symlink():
return
if not ipath.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
if not Path(ipath).name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
return

container = factory(Path(ipath).resolve())
container.open(ipath, 'rb')
if ipath.lower().endswith(('.xci', '.xcz')):
container.open(ipath)

if ipath.name.lower().endswith(('.xci', '.xcz')):
container = container.hfs0['secure']

try:
Expand Down
94 changes: 24 additions & 70 deletions py/ns_verify_folder.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
#! /usr/bin/python3

import os
import sys
import json
import requests
import re

from pathlib import Path
import argparse
import requests
import json
import sys

from nstools.nut import Keys

from nstools.lib import Verify
from nsz.nut import Keys
from nstools import Verify

# set app path
appPath = Path(sys.argv[0])
while not appPath.is_dir():
appPath = appPath.parents[0]
appPath = os.path.abspath(appPath)
appPath = Path(appPath).resolve().as_posix()
print(f'[:INFO:] App Path: {appPath}')

# set logs path
# logs_dir = os.path.abspath(os.path.join(appPath, '..', 'logs'))
# print(f'[:INFO:] Logs Path: {logs_dir}')

import argparse
# set args
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-i', '--input', help = 'input folder')
parser.add_argument('-w', '--webhook-url', help = 'discord webhook url', required = False)
Expand Down Expand Up @@ -58,83 +51,45 @@ def send_hook(message_content: str = '', PadPrint: bool = False):
pass

def scan_folder():
ipath = os.path.abspath(INCP_PATH)
fname = os.path.basename(ipath).upper()

# lpath_badfolder = os.path.join(logs_dir, 'bad-folder.log')
# lpath_badname = os.path.join(logs_dir, 'bad-names.log')
# lpath_badfile = os.path.join(logs_dir, 'bad-file.log')

# if not os.path.exists(logs_dir):
# os.makedirs(logs_dir)
ipath = Path(INCP_PATH).resolve().as_posix()

# if os.path.exists(lpath_badfolder):
# os.remove(lpath_badfolder)
# if os.path.exists(lpath_badname):
# os.remove(lpath_badname)
# if os.path.exists(lpath_badfile):
# os.remove(lpath_badfile)

if not os.path.exists(ipath):
if not Path(ipath).exists():
print(f'[:WARN:] Please put your files in "{ipath}" and run this script again.')
return

files = list()
for item in sorted(os.listdir(ipath)):
item_path = os.path.join(ipath, item)
if not os.path.isfile(item_path):

for item in sorted(list(Path(ipath).iterdir())):
item_path = Path(item)
if not item_path.is_file() or item_path.is_symlink():
continue
if not item.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
if not item_path.name.lower().endswith(('.xci', '.xcz', '.nsp', '.nsz')):
continue
files.append(item)
files.append(item.as_posix())

findex = 0
for item in sorted(files):
item_path = os.path.join(ipath, item)
item_path = Path(item)

findex += 1
send_hook(f'[:INFO:] File found ({findex} of {len(files)}): {item_path}', True)
send_hook(f'[:INFO:] File found ({findex} of {len(files)}): {item_path.name}', True)
send_hook(f'[:INFO:] Checking filename...')

data = Verify.parse_name(item)
data = Verify.parse_name(item_path.name)

if data is None:
send_hook(f'{item_path}: BAD NAME')
# with open(lpath_badname, 'a') as f:
# f.write(f'{item_path}\n')
send_hook(f'{item_path.name}: BAD NAME')

# if data is not None and re.match(r'^BASE|UPD(ATE)?|DLC|XCI$', fname) is not None:
# if item.lower().endswith(('.xci', '.xcz')):
# iscart = True
# else:
# iscart = False
# if fname == 'UPDATE':
# fname = 'UPD'
# if fname == 'BASE' and data['title_type'] != 'BASE' or fname == 'BASE' and iscart == True:
# with open(lpath_badfolder, 'a') as f:
# f.write(f'{item_path}\n')
# if fname == 'UPD' and data['title_type'] != 'UPD' or fname == 'UPD' and iscart == True:
# with open(lpath_badfolder, 'a') as f:
# f.write(f'{item_path}\n')
# if fname == 'DLC' and data['title_type'] != 'DLC' or fname == 'DLC' and iscart == True:
# with open(lpath_badfolder, 'a') as f:
# f.write(f'{item_path}\n')
# if fname == 'XCI' and iscart == False:
# with open(lpath_badfolder, 'a') as f:
# f.write(f'{item_path}\n')

rootpath = os.path.dirname(item_path)
basename = os.path.basename(item_path)
rootpath = item_path.parent.as_posix()
basename = item_path.name
basename = f'{basename[:-4]}-{basename[-3:]}-verify'
log_name = os.path.join(rootpath, basename)
log_name = Path(rootpath).joinpath(basename).as_posix()

try:
send_hook(f'[:INFO:] Verifying...')
nspTest, nspLog = Verify.verify(item_path)
nspTest, nspLog = Verify.verify(item_path.as_posix())
if nspTest != True:
send_hook(f'{item_path}: BAD', True)
# with open(lpath_badfile, 'a') as f:
# f.write(f'{item_path}\n')
else:
send_hook(f'{item_path}: OK', True)
if SAVE_VLOG == True:
Expand All @@ -147,7 +102,6 @@ def scan_folder():
except Exception as e:
send_hook(f'[:WARN:] An error occurred:\n{item_path}: {str(e)}')


if __name__ == "__main__":
if INCP_PATH:
scan_folder()
Expand Down
Loading
Loading