diff --git a/.gitignore b/.gitignore index 67b4408..e3947bb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ /build/ /dist/ /vntools.egg-info/ -/.idea/ -/.vscode/ \ No newline at end of file diff --git a/PKGBUILD b/PKGBUILD index f0bb5f2..0c6ef83 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -3,7 +3,7 @@ _pkgname=VNTools pkgname=vntools-git -pkgver=2.1.ab883708 +pkgver=2.0.e5bf961 pkgrel=1 pkgdesc="Collection of tools used by VienDesu! Porting Team" arch=("any") @@ -16,7 +16,7 @@ sha256sums=("SKIP") pkgver() { cd "${srcdir}/${_pkgname}" - printf "2.1.%s" "$(git rev-parse --short HEAD)" + printf "2.0.%s" "$(git rev-parse --short HEAD)" } build() { diff --git a/README.md b/README.md index eb026d5..f9b3c17 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ Collection of tools used by VienDesu! Porting Team ### Installation #### Download from releases: - * Windows - `TODO` - * Linux - `TODO` - * MacOS - `TODO` + * Windows - [x64](https://git.viende.su/VienDesuPorting/VNTools/releases/download/2.0.0/vntools-win-x64.zip) + * Linux - [x86_64](https://git.viende.su/VienDesuPorting/VNTools/releases/download/2.0.0/vntools-linux-x86_64.zip) [arm64](https://git.viende.su/VienDesuPorting/VNTools/releases/download/2.0.0/vntools-linux-arm64.zip) + * MacOS - [x86_64](https://git.viende.su/VienDesuPorting/VNTools/releases/download/2.0.0/vntools-darwin-x86_64.zip) [arm64](https://git.viende.su/VienDesuPorting/VNTools/releases/download/2.0.0/vntools-darwin-arm64.zip) #### Build tools as binaries: - * Run `./build.sh` on UNIX + * Run `./build.sh` for UNIX * Run `.\build.bat` for Windows #### Install as python package: diff --git a/build.bat b/build.bat index 9c097ac..821ed6c 100755 --- a/build.bat +++ b/build.bat @@ -12,8 +12,6 @@ python -m nuitka --jobs=%NUMBER_OF_PROCESSORS% --output-dir=output --follow-impo move /Y output\unrenapk.exe output\bin python -m nuitka --jobs=%NUMBER_OF_PROCESSORS% --output-dir=output --follow-imports --onefile --output-filename=vnds2renpy vnds2renpy || goto :exit move /Y output\vnds2renpy.exe output\bin -python -m nuitka --jobs=%NUMBER_OF_PROCESSORS% --output-dir=output --follow-imports --onefile --output-filename=rpatool rpatool || goto :exit -move /Y output\rpatool.exe output\bin echo "Done! You can get binaries into output\bin directory" :venv_error diff --git a/build.sh b/build.sh index 83e0e1c..ffbd2b4 100755 --- a/build.sh +++ b/build.sh @@ -21,6 +21,4 @@ python3 -m nuitka "${jobs}" --output-dir=output --onefile --follow-imports --out mv output/unrenapk output/bin python3 -m nuitka "${jobs}" --output-dir=output --onefile --follow-imports --output-filename=vnds2renpy vnds2renpy mv output/vnds2renpy output/bin -python3 -m nuitka "${jobs}" --output-dir=output --onefile --follow-imports --output-filename=rpatool rpatool -mv output/rpatool output/bin echo "Done! You can get binaries into output/bin directory" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 59ab9fa..01df088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,24 +5,22 @@ requires = [ build-backend = "setuptools.build_meta" [tool.setuptools] -packages = ["vnrecode", "unrenapk", "vnds2renpy", "rpatool"] +packages = ["vnrecode", "unrenapk", "vnds2renpy"] include-package-data = true [tool.setuptools.package-data] 'vnrecode' = ['*.py'] 'vnds2renpy' = ['*.py'] 'unrenapk' = ['*.py'] -'rpatool' = ['*.py'] [project.scripts] vnrecode = "vnrecode.__main__:init" vnds2renpy = "vnds2renpy.__main__:main" unrenapk = "unrenapk.application:launch" -rpatool = "rpatool.rpatool:main" [project] name = "vntools" -version = "2.1-dev" +version = "2.0.0" requires-python = ">= 3.11" dependencies = [ "Pillow>=10.3.0", diff --git a/rpatool/README.md b/rpatool/README.md deleted file mode 100644 index 093711f..0000000 --- a/rpatool/README.md +++ /dev/null @@ -1,124 +0,0 @@ -rpatool -======= - -This is modified version of a [rpatool](https://github.com/shizmob/rpatool) by [shizmob](https://github.com/shizmob). - -This is a simple tool allowing you to create, modify and extract Ren'Py Archive (.rpa/.rpi) files. -Currently, only writing to RPAv2/RPAv3 archives is supported. - -Usage ------ - rpatool [-l|-x|-c|-d|-a] [-o OUTFILE] [-2] [-3] [-k KEY] - [-p COUNT] [-h] [-v] [-V] - ARCHIVE [FILE [FILE ...]] - - - positional arguments: - ARCHIVE The Ren'py archive file to operate on - FILE Zero or more files to operate on - - actions: - -l, --list List files in archive ARCHIVE - -x, --extract Extract FILEs from ARCHIVE - -c, --create Creative ARCHIVE from FILEs - -d, --delete Delete FILEs from ARCHIVE - -a, --append Append FILEs to ARCHIVE - - optional arguments: - -o OUTFILE, --outfile OUTFILE - An alternative output archive file when appending to or - deleting from archives, or output directory when extracting. - -2, --two Use the RPAv2 format for creating/appending to - archives - -3, --three Use the RPAv3 format for creating/appending to - archives (default) - -k KEY, --key KEY The obfuscation key used for creating RPAv3 archives - (default: 0xDEADBEEF) - -p COUNT, --padding COUNT - The maximum number of bytes of padding to add between - files (default: 0) - --all If specified, extracts all .rpa archives in the current directory - -h, --help Print this help and exit - -v, --verbose Be a bit more verbose while performing operations - -V, --version Show version information - - The FILE argument can optionally be in ARCHIVE=REAL format, mapping a file in - the archive file system to a file on your real file system. An example of - this is: rpatool -x test.rpa script.rpyc=/home/foo/test.rpyc - -Examples --------- - rpatool -x foo.rpa -Will extract every file from `foo.rpa`into the current directory, making subdirectories when necessary. - - rpatool -o output -x foo.rpa script.rpyc ui.png -Will extract the files `script.rpyc` and `ui.png` from `foo.rpa` into the directory `output`. - - rpatool -c bar.rpa test.jpg script.rpy sprites -Will create the archive `bar.rpa`, containing the files `test.jpg`, `script.rpy` and the directory `sprites`. - - rpatool -p 25 -k 12345 -c bar.rpa movies=C:\projects\vn\movies -Will create the archive `bar.rpa` with the obfuscation key `0x12345` and maximum padding of `25`, taking files from `C:\projects\vn\movies` and placing them in the archive folder `movies`. - - rpatool -l baz.rpa -Will list all files in the archive `baz.rpa`. - - rpatool -v -a foo.rpa sprites=sprites_new -Will add all files from the directory `sprites_new` to the directory `sprites` in the archive, giving more information about what it's doing. - - rpatool -o bar_new.rpa -d bar.rpa foo.jpg -Will remove the file `foo.jpg` from the archive `bar.rpa`, storing the result archive in `bar_new.rpa`. - - rpatool -all game/ -Will extract all the files from all .rpa archives in a `game/` folder. - -API ---- -`rpatool` can also be included in any other project (following the license conditions, of course) to provide the `RenPyArchive` class. -A small overview: - - RenPyArchive([file = None], [version = 3], [padlength = 0], [key = 0xDEADBEEF], [verbose = False]) -The constructor, which will optionally load an archive file. - -`file`: the archive file to open. If None, no archive will be attempted to open. - -`version`: the archive format version used to save the archive when `RenPyArchive.save([file])` is called. Default: 3 - -`padlength`: the maximum number of bytes of padding to put between files when saving. Default: 0 - -`key`: the obfuscation key used when saving RPAv3 archives. Default: 0xDEADBEEF - -`verbose`: print info on what we are doing to the command line. Default: False - - RenPyArchive.load(filename) -Loads an archive file from `filename`. Will raise an `IOError` if the file can't be accessed, or a `ValueError` if the file is not detected as a Ren'Py archive. - - RenPyArchive.save([filename]) -Save the archive to `filename`. Will raise `ValueError` if the filename isn't given with `filename`, nor previously defined, or an `IOError` if it couldn't save the file. - - RenPyArchive.list() -Give a list of all filenames currently in the archive. - - RenPyArchive.has_file(filename) -Returns True if `filename` is found in the archive, False otherwhise. - - RenPyArchive.add(filename, content) -Add a file to the archive with file `filename` and contents `content`. Will raise a `ValueError` if the filename already exists in the archive. - - RenPyArchive.change(filename, content) -Change the contents of a current file in the archive. Will raise an `IOError` if the file isn't known in the archive. - - RenPyArchive.remove(filename) -Remove `filename` from the archive. Will raise an `IOError` if the filename isn't known in the archive. - - RenPyArchive.read(filename) -Read and return the content of file `filename` in the archive. Will raise an `IOError` if the filename isn't known in the archive. - -Disclaimer ----------- -This tool is intended for use with files on which the authors allowed modification of and/or extraction from ONLY and the unpermitted use on files where such consent was not given is highly discouraged, and most likely a license violation as well. -Support requests for help with dealing with such files will not be answered. - -Credits -------- -Credits for the creation of the Ren'Py archive format and the reference code in Ren'Py go to renpytom. diff --git a/rpatool/__init__.py b/rpatool/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/rpatool/__main__.py b/rpatool/__main__.py deleted file mode 100644 index b76bbbb..0000000 --- a/rpatool/__main__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -from rpatool import rpatool - -if __name__ == '__main__': - rpatool.main() \ No newline at end of file diff --git a/rpatool/rpatool.py b/rpatool/rpatool.py deleted file mode 100644 index 841fe54..0000000 --- a/rpatool/rpatool.py +++ /dev/null @@ -1,493 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import print_function -import sys -import os -import codecs -import errno -import random -try: - import pickle5 as pickle -except: - import pickle - if sys.version_info < (3, 8): - print('warning: pickle5 module could not be loaded and Python version is < 3.8,', file=sys.stderr) - print(' newer Ren\'Py games may fail to unpack!', file=sys.stderr) - if sys.version_info >= (3, 5): - print(' if this occurs, fix it by installing pickle5:', file=sys.stderr) - print(' {} -m pip install pickle5'.format(sys.executable), file=sys.stderr) - else: - print(' if this occurs, please upgrade to a newer Python (>= 3.5).', file=sys.stderr) - print(file=sys.stderr) - -if sys.version_info[0] >= 3: # Python 3 - def _unicode(text): - return text - - def _printable(text): - return text - - def _unmangle(data): - if type(data) == bytes: - return data - else: - return data.encode('latin1') - - def _unpickle(data): - # Specify latin1 encoding to prevent raw byte values from causing an ASCII decode error. - return pickle.loads(data, encoding='latin1') - -elif sys.version_info[0] == 2: # Python 2 - def _unicode(text): - if isinstance(text, unicode): # You can ignore 'Unresolved reference', because you never hit this block actually - return text - return text.decode('utf-8') - - def _printable(text): - return text.encode('utf-8') - - def _unmangle(data): - return data - - def _unpickle(data): - return pickle.loads(data) - -class RenPyArchive: - file = None - handle = None - - files = {} - indexes = {} - - version = None - padlength = 0 - key = None - verbose = False - - RPA2_MAGIC = 'RPA-2.0 ' - RPA3_MAGIC = 'RPA-3.0 ' - RPA3_2_MAGIC = 'RPA-3.2 ' - - # For backward compatibility, otherwise Python3-packed archives won't be read by Python2 - PICKLE_PROTOCOL = 2 - - def __init__(self, file=None, version=3, padlength=0, key=0xDEADBEEF, verbose=False): - self.padlength = padlength - self.key = key - self.verbose = verbose - - if file is not None: - self.load(file) - else: - self.version = version - - def __del__(self): - if self.handle is not None: - self.handle.close() - - # Determine archive version. - def get_version(self): - self.handle.seek(0) - magic = self.handle.readline().decode('utf-8') - - if magic.startswith(self.RPA3_2_MAGIC): - return 3.2 - elif magic.startswith(self.RPA3_MAGIC): - return 3 - elif magic.startswith(self.RPA2_MAGIC): - return 2 - elif self.file.endswith('.rpi'): - return 1 - - raise ValueError('the given file is not a valid Ren\'Py archive, or an unsupported version') - - # Extract file indexes from opened archive. - def extract_indexes(self): - self.handle.seek(0) - indexes = None - - if self.version in [2, 3, 3.2]: - # Fetch metadata. - metadata = self.handle.readline() - vals = metadata.split() - offset = int(vals[1], 16) - if self.version == 3: - self.key = 0 - for subkey in vals[2:]: - self.key ^= int(subkey, 16) - elif self.version == 3.2: - self.key = 0 - for subkey in vals[3:]: - self.key ^= int(subkey, 16) - - # Load in indexes. - self.handle.seek(offset) - contents = codecs.decode(self.handle.read(), 'zlib') - indexes = _unpickle(contents) - - # Deobfuscate indexes. - if self.version in [3, 3.2]: - obfuscated_indexes = indexes - indexes = {} - for i in obfuscated_indexes.keys(): - if len(obfuscated_indexes[i][0]) == 2: - indexes[i] = [ (offset ^ self.key, length ^ self.key) for offset, length in obfuscated_indexes[i] ] - else: - indexes[i] = [ (offset ^ self.key, length ^ self.key, prefix) for offset, length, prefix in obfuscated_indexes[i] ] - else: - indexes = pickle.loads(codecs.decode(self.handle.read(), 'zlib')) - - return indexes - - # Generate pseudorandom padding (for whatever reason). - def generate_padding(self): - length = random.randint(1, self.padlength) - - padding = '' - while length > 0: - padding += chr(random.randint(1, 255)) - length -= 1 - - return bytes(padding, 'utf-8') - - # Converts a filename to archive format. - def convert_filename(self, filename): - (drive, filename) = os.path.splitdrive(os.path.normpath(filename).replace(os.sep, '/')) - return filename - - # Debug (verbose) messages. - def verbose_print(self, message): - if self.verbose: - print(message) - - # List files in archive and current internal storage. - def list(self): - return list(self.indexes.keys()) + list(self.files.keys()) - - # Check if a file exists in the archive. - def has_file(self, filename): - filename = _unicode(filename) - return filename in self.indexes.keys() or filename in self.files.keys() - - # Read file from archive or internal storage. - def read(self, filename): - filename = self.convert_filename(_unicode(filename)) - - # Check if the file exists in our indexes. - if filename not in self.files and filename not in self.indexes: - raise IOError(errno.ENOENT, 'the requested file {0} does not exist in the given Ren\'Py archive'.format( - _printable(filename))) - - # If it's in our opened archive index, and our archive handle isn't valid, something is obviously wrong. - if filename not in self.files and filename in self.indexes and self.handle is None: - raise IOError(errno.ENOENT, 'the requested file {0} does not exist in the given Ren\'Py archive'.format( - _printable(filename))) - - # Check our simplified internal indexes first, in case someone wants to read a file they added before without saving. - if filename in self.files: - self.verbose_print('Reading file {0} from internal storage...'.format(_printable(filename))) - return self.files[filename] - else: - # Read offset and length, seek to the offset and read the file contents. - if len(self.indexes[filename][0]) == 3: - (offset, length, prefix) = self.indexes[filename][0] - else: - (offset, length) = self.indexes[filename][0] - prefix = '' - - self.verbose_print('Reading file {0} from data file {1}... (offset = {2}, length = {3} bytes)'.format( - _printable(filename), self.file, offset, length)) - self.handle.seek(offset) - return _unmangle(prefix) + self.handle.read(length - len(prefix)) - - # Modify a file in archive or internal storage. - def change(self, filename, contents): - filename = _unicode(filename) - self.remove(filename) - self.add(filename, contents) - - # Add a file to the internal storage. - def add(self, filename, contents): - filename = self.convert_filename(_unicode(filename)) - if filename in self.files or filename in self.indexes: - raise ValueError('file {0} already exists in archive'.format(_printable(filename))) - self.verbose_print('Adding file {0} to archive... (length = {1} bytes)'.format( - _printable(filename), len(contents))) - self.files[filename] = contents - - # Remove a file from archive or internal storage. - def remove(self, filename): - filename = _unicode(filename) - if filename in self.files: - self.verbose_print('Removing file {0} from internal storage...'.format(_printable(filename))) - del self.files[filename] - elif filename in self.indexes: - self.verbose_print('Removing file {0} from archive indexes...'.format(_printable(filename))) - del self.indexes[filename] - else: - raise IOError(errno.ENOENT, 'the requested file {0} does not exist in this archive'.format(_printable(filename))) - - # Load archive. - def load(self, filename): - filename = _unicode(filename) - - if self.handle is not None: - self.handle.close() - self.file = filename - self.files = {} - self.handle = open(self.file, 'rb') - self.version = self.get_version() - self.indexes = self.extract_indexes() - - # Save current state into a new file, merging archive and internal storage, rebuilding indexes, and optionally saving in another format version. - def save(self, filename=None): - filename = _unicode(filename) - - if filename is None: - filename = self.file - if filename is None: - raise ValueError('no target file found for saving archive') - if self.version != 2 and self.version != 3: - raise ValueError('saving is only supported for version 2 and 3 archives') - - self.verbose_print('Rebuilding archive index...') - # Merge files added or changed in this session. - files = self.files - # First, read files from the current archive into our files structure. - for file in list(self.indexes.keys()): - content = self.read(file) - del self.indexes[file] - files[file] = content - - # Predict header length; we will write it last. - offset = 0 - if self.version == 3: - offset = 34 - elif self.version == 2: - offset = 25 - archive = open(filename, 'wb') - archive.seek(offset) - - # Build new indexes while writing files to the archive. - indexes = {} - self.verbose_print('Writing files to archive file...') - for file, content in files.items(): - # Generate random padding if needed. - if self.padlength > 0: - padding = self.generate_padding() - archive.write(padding) - offset += len(padding) - - archive.write(content) - # Update index. - if self.version == 3: - indexes[file] = [ (offset ^ self.key, len(content) ^ self.key) ] - elif self.version == 2: - indexes[file] = [ (offset, len(content)) ] - offset += len(content) - - # Write the indexes. - self.verbose_print('Writing archive index to archive file...') - archive.write(codecs.encode(pickle.dumps(indexes, self.PICKLE_PROTOCOL), 'zlib')) - # Now write the header. - self.verbose_print('Writing header to archive file... (version = RPAv{0})'.format(self.version)) - archive.seek(0) - if self.version == 3: - archive.write(codecs.encode('{}{:016x} {:08x}\n'.format(self.RPA3_MAGIC, offset, self.key))) - else: - archive.write(codecs.encode('{}{:016x}\n'.format(self.RPA2_MAGIC, offset))) - archive.close() - - # Reload the file in our internal database. - self.load(filename) - - -def main(): - import argparse - - parser = argparse.ArgumentParser( - description='A tool for working with Ren\'Py archive files.', - epilog='The FILE argument can optionally be in ARCHIVE=REAL format, mapping a file in the archive filesystem to a file on your system. For example: rpatool -x test.rpa script.rpyc=/home/foo/test.rpyc', - add_help=False) - - parser.add_argument('archive', metavar='ARCHIVE', nargs='?', help='The Ren\'Py archive file to operate on.') - parser.add_argument('files', metavar='FILE', nargs='*', action='append', help='Zero or more files to operate on.') - - parser.add_argument('-l', '--list', action='store_true', help='List files in archive ARCHIVE.') - parser.add_argument('-x', '--extract', action='store_true', help='Extract FILEs from ARCHIVE.') - parser.add_argument('-c', '--create', action='store_true', help='Create ARCHIVE from FILEs.') - parser.add_argument('-d', '--delete', action='store_true', help='Delete FILEs from ARCHIVE.') - parser.add_argument('-a', '--append', action='store_true', help='Append FILEs to ARCHIVE.') - - parser.add_argument('-2', '--two', action='store_true', help='Use the RPAv2 format for creating/appending archives.') - parser.add_argument('-3', '--three', action='store_true', help='Use the RPAv3 format for creating/appending archives (default).') - - parser.add_argument('-k', '--key', metavar='KEY', help='The obfuscation key used for creating RPAv3 archives, in hexadecimal (default: 0xDEADBEEF).') - parser.add_argument('-p', '--padding', metavar='COUNT', help='The maximum number of padding bytes to add between files (default: 0).') - parser.add_argument('-o', '--outfile', help='An alternative output archive file when appending or deleting from archives, or output directory when extracting.') - - # Add new flag for extracting all archives in the current directory. - parser.add_argument('--all', action='store_true', help='If specified, extracts all .rpa archives in the current directory.') - - parser.add_argument('-h', '--help', action='help', help='Print this help message and exit.') - parser.add_argument('-v', '--verbose', action='store_true', help='Show detailed messages during operations.') - parser.add_argument('-V', '--version', action='version', version='rpatool v0.8', help='Show version information.') - arguments = parser.parse_args() - - # Determine RPA version. - if arguments.two: - version = 2 - else: - version = 3 - - # Determine RPAv3 key. - if hasattr(arguments, 'key') and arguments.key is not None: - key = int(arguments.key, 16) - else: - key = 0xDEADBEEF - - # Determine padding bytes. - if hasattr(arguments, 'padding') and arguments.padding is not None: - padding = int(arguments.padding) - else: - padding = 0 - - # If the --all flag is specified, ignore the ARCHIVE argument and process all .rpa files in the current directory. - if arguments.all: - archive_files = [f for f in os.listdir('.') if f.lower().endswith('.rpa')] - if not archive_files: - print('No .rpa archives found in the current directory.', file=sys.stderr) - sys.exit(0) - for archive_filename in archive_files: - print("Processing archive: {}".format(archive_filename)) - try: - current_archive = RenPyArchive(archive_filename, padlength=padding, key=key, version=version, verbose=arguments.verbose) - except IOError as e: - print('Failed to open archive {}: {}'.format(archive_filename, e), file=sys.stderr) - continue - - # Determine the extraction directory. - # If --outfile is provided, use it; otherwise, extract to the current directory directly. - if arguments.outfile: - outfolder = _unicode(arguments.outfile) - else: - outfolder = "." - - if not os.path.exists(outfolder): - os.makedirs(outfolder) - - # Extract all files from the archive. - for filename in current_archive.list(): - if filename.find('=') != -1: - (outfile, filename) = filename.split('=', 2) - else: - outfile = filename - try: - contents = current_archive.read(filename) - destination = os.path.join(outfolder, outfile) - destdir = os.path.dirname(destination) - if destdir and not os.path.exists(destdir): - os.makedirs(destdir) - with open(destination, 'wb') as file_out: - file_out.write(contents) - except Exception as e: - print('Failed to extract file {} from {}: {}'.format(filename, archive_filename, e), file=sys.stderr) - sys.exit(0) - - # Determine input archive/output file. - if arguments.create: - archive = None - output = _unicode(arguments.archive) - else: - archive = _unicode(arguments.archive) if arguments.archive is not None else None - if hasattr(arguments, 'outfile') and arguments.outfile is not None: - output = _unicode(arguments.outfile) - else: - if arguments.extract: - output = '.' - else: - output = _unicode(arguments.archive) if arguments.archive is not None else '' - - # Normalize file arguments. - if len(arguments.files) > 0 and isinstance(arguments.files[0], list): - arguments.files = arguments.files[0] - - try: - if archive is None: - raise ValueError("No input archive specified.") - archive = RenPyArchive(archive, padlength=padding, key=key, version=version, verbose=arguments.verbose) - except Exception as e: - print('Failed to open archive {} for reading: {}'.format(archive, e), file=sys.stderr) - sys.exit(1) - - if arguments.create or arguments.append: - def add_file(filename): - if filename.find('=') != -1: - (outfile, filename) = filename.split('=', 2) - else: - outfile = filename - - if os.path.isdir(filename): - for file in os.listdir(filename): - add_file(outfile + os.sep + file + '=' + filename + os.sep + file) - else: - try: - with open(filename, 'rb') as file_in: - archive.add(outfile, file_in.read()) - except Exception as e: - print('Failed to add file {} to archive: {}'.format(filename, e), file=sys.stderr) - - for filename in arguments.files: - add_file(_unicode(filename)) - - archive.version = version - try: - archive.save(output) - except Exception as e: - print('Failed to save archive: {}'.format(e), file=sys.stderr) - elif arguments.delete: - for filename in arguments.files: - try: - archive.remove(filename) - except Exception as e: - print('Failed to delete file {} from archive: {}'.format(filename, e), file=sys.stderr) - archive.version = version - try: - archive.save(output) - except Exception as e: - print('Failed to save archive: {}'.format(e), file=sys.stderr) - elif arguments.extract: - # If specific files are provided, extract them; otherwise, extract all files. - if len(arguments.files) > 0: - files = arguments.files - else: - files = archive.list() - - if not os.path.exists(output): - os.makedirs(output) - - for filename in files: - if filename.find('=') != -1: - (outfile, filename) = filename.split('=', 2) - else: - outfile = filename - try: - contents = archive.read(filename) - out_path = os.path.join(output, outfile) - if os.path.dirname(out_path) and not os.path.exists(os.path.dirname(out_path)): - os.makedirs(os.path.dirname(out_path)) - with open(out_path, 'wb') as file_out: - file_out.write(contents) - except Exception as e: - print('Failed to extract file {} from archive: {}'.format(filename, e), file=sys.stderr) - elif arguments.list: - file_list = archive.list() - file_list.sort() - for file in file_list: - print(file) - else: - print('No operation specified :(') - print('Use {} --help for usage details.'.format(sys.argv[0])) - -if __name__ == "__main__": - main() \ No newline at end of file