Add Python 3 support
This commit is contained in:
parent
fc78e81f39
commit
fd3aebd7d3
1 changed files with 127 additions and 95 deletions
104
rpatool
104
rpatool
|
@ -1,11 +1,29 @@
|
||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import codecs
|
||||||
import pickle
|
import pickle
|
||||||
import errno
|
import errno
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
def _ensure_unicode(text):
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _prepare_unicode_for_print(text):
|
||||||
|
return text
|
||||||
|
elif sys.version_info[0] == 2:
|
||||||
|
def _ensure_unicode(text):
|
||||||
|
if isinstance(text, unicode):
|
||||||
|
return text
|
||||||
|
return unicode(text, 'utf-8')
|
||||||
|
|
||||||
|
def _prepare_unicode_for_print(text):
|
||||||
|
return text.encode('utf-8')
|
||||||
|
|
||||||
class RenPyArchive:
|
class RenPyArchive:
|
||||||
file = None
|
file = None
|
||||||
handle = None
|
handle = None
|
||||||
|
@ -21,6 +39,9 @@ class RenPyArchive:
|
||||||
RPA2_MAGIC = 'RPA-2.0 '
|
RPA2_MAGIC = 'RPA-2.0 '
|
||||||
RPA3_MAGIC = 'RPA-3.0 '
|
RPA3_MAGIC = 'RPA-3.0 '
|
||||||
|
|
||||||
|
# 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):
|
def __init__(self, file = None, version = 3, padlength = 0, key = 0xDEADBEEF, verbose = False):
|
||||||
self.padlength = padlength
|
self.padlength = padlength
|
||||||
self.key = key
|
self.key = key
|
||||||
|
@ -66,7 +87,7 @@ class RenPyArchive:
|
||||||
|
|
||||||
# Load in indexes.
|
# Load in indexes.
|
||||||
self.handle.seek(offset)
|
self.handle.seek(offset)
|
||||||
indexes = pickle.loads(self.handle.read().decode('zlib'))
|
indexes = pickle.loads(codecs.decode(self.handle.read(), 'zlib'))
|
||||||
|
|
||||||
# Deobfuscate indexes.
|
# Deobfuscate indexes.
|
||||||
if self.version == 3:
|
if self.version == 3:
|
||||||
|
@ -78,7 +99,7 @@ class RenPyArchive:
|
||||||
else:
|
else:
|
||||||
indexes[i] = [ (offset ^ self.key, length ^ self.key, prefix) for offset, length, prefix in obfuscated_indexes[i] ]
|
indexes[i] = [ (offset ^ self.key, length ^ self.key, prefix) for offset, length, prefix in obfuscated_indexes[i] ]
|
||||||
else:
|
else:
|
||||||
indexes = pickle.loads(self.handle.read().decode('zlib'))
|
indexes = pickle.loads(codecs.decode(self.handle.read(), 'zlib'))
|
||||||
|
|
||||||
return indexes
|
return indexes
|
||||||
|
|
||||||
|
@ -106,27 +127,30 @@ class RenPyArchive:
|
||||||
|
|
||||||
# List files in archive and current internal storage.
|
# List files in archive and current internal storage.
|
||||||
def list(self):
|
def list(self):
|
||||||
return self.indexes.keys() + self.files.keys()
|
return list(self.indexes.keys()) + list(self.files.keys())
|
||||||
|
|
||||||
# Check if a file exists in the archive.
|
# Check if a file exists in the archive.
|
||||||
def has_file(self, filename):
|
def has_file(self, filename):
|
||||||
|
filename = _ensure_unicode(filename)
|
||||||
return filename in self.indexes.keys() or filename in self.files.keys()
|
return filename in self.indexes.keys() or filename in self.files.keys()
|
||||||
|
|
||||||
# Read file from archive or internal storage.
|
# Read file from archive or internal storage.
|
||||||
def read(self, filename):
|
def read(self, filename):
|
||||||
filename = self.convert_filename(filename)
|
filename = self.convert_filename(_ensure_unicode(filename))
|
||||||
|
|
||||||
# Check if the file exists in our indexes.
|
# Check if the file exists in our indexes.
|
||||||
if filename not in self.files and filename not in self.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(filename))
|
raise IOError(errno.ENOENT, 'the requested file {0} does not exist in the given Ren\'Py archive'.format(
|
||||||
|
_prepare_unicode_for_print(filename)))
|
||||||
|
|
||||||
# If it's in our opened archive index, and our archive handle isn't valid, something is obviously wrong.
|
# 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:
|
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(filename))
|
raise IOError(errno.ENOENT, 'the requested file {0} does not exist in the given Ren\'Py archive'.format(
|
||||||
|
_prepare_unicode_for_print(filename)))
|
||||||
|
|
||||||
# Check our simplified internal indexes first, in case someone wants to read a file they added before without saving, for some unholy reason.
|
# Check our simplified internal indexes first, in case someone wants to read a file they added before without saving, for some unholy reason.
|
||||||
if filename in self.files:
|
if filename in self.files:
|
||||||
self.verbose_print('Reading file {0} from internal storage...'.format(filename.encode('utf-8')))
|
self.verbose_print('Reading file {0} from internal storage...'.format(_prepare_unicode_for_print(filename)))
|
||||||
return self.files[filename]
|
return self.files[filename]
|
||||||
# We need to read the file from our open archive.
|
# We need to read the file from our open archive.
|
||||||
else:
|
else:
|
||||||
|
@ -137,39 +161,45 @@ class RenPyArchive:
|
||||||
(offset, length) = self.indexes[filename][0]
|
(offset, length) = self.indexes[filename][0]
|
||||||
prefix = ''
|
prefix = ''
|
||||||
|
|
||||||
self.verbose_print('Reading file {0} from data file {1}... (offset = {2}, length = {3} bytes)'.format(filename.encode('utf-8'), self.file, offset, length))
|
self.verbose_print('Reading file {0} from data file {1}... (offset = {2}, length = {3} bytes)'.format(
|
||||||
|
_prepare_unicode_for_print(filename), self.file, offset, length))
|
||||||
self.handle.seek(offset)
|
self.handle.seek(offset)
|
||||||
return prefix + self.handle.read(length - len(prefix))
|
return codecs.encode(prefix) + self.handle.read(length - len(prefix))
|
||||||
|
|
||||||
# Modify a file in archive or internal storage.
|
# Modify a file in archive or internal storage.
|
||||||
def change(self, filename, contents):
|
def change(self, filename, contents):
|
||||||
|
filename = _ensure_unicode(filename)
|
||||||
|
|
||||||
# Our 'change' is basically removing the file from our indexes first, and then re-adding it.
|
# Our 'change' is basically removing the file from our indexes first, and then re-adding it.
|
||||||
self.remove(filename)
|
self.remove(filename)
|
||||||
self.add(filename, contents)
|
self.add(filename, contents)
|
||||||
|
|
||||||
# Add a file to the internal storage.
|
# Add a file to the internal storage.
|
||||||
def add(self, filename, contents):
|
def add(self, filename, contents):
|
||||||
filename = unicode(self.convert_filename(filename), 'utf-8')
|
filename = self.convert_filename(_ensure_unicode(filename))
|
||||||
if filename in self.files or filename in self.indexes:
|
if filename in self.files or filename in self.indexes:
|
||||||
raise ValueError('file {0} already exists in archive'.format(filename))
|
raise ValueError('file {0} already exists in archive'.format(_prepare_unicode_for_print(filename)))
|
||||||
|
|
||||||
self.verbose_print('Adding file {0} to archive... (length = {1} bytes)'.format(filename.encode('utf-8'), len(contents)))
|
self.verbose_print('Adding file {0} to archive... (length = {1} bytes)'.format(
|
||||||
|
_prepare_unicode_for_print(filename), len(contents)))
|
||||||
self.files[filename] = contents
|
self.files[filename] = contents
|
||||||
|
|
||||||
# Remove a file from archive or internal storage.
|
# Remove a file from archive or internal storage.
|
||||||
def remove(self, filename):
|
def remove(self, filename):
|
||||||
filename = unicode(self.convert_filename(filename), 'utf-8')
|
filename = _ensure_unicode(filename)
|
||||||
if filename in self.files:
|
if filename in self.files:
|
||||||
self.verbose_print('Removing file {0} from internal storage...'.format(filename.encode('utf-8')))
|
self.verbose_print('Removing file {0} from internal storage...'.format(_prepare_unicode_for_print(filename)))
|
||||||
del self.files[filename]
|
del self.files[filename]
|
||||||
elif filename in self.indexes:
|
elif filename in self.indexes:
|
||||||
self.verbose_print('Removing file {0} from archive indexes...'.format(filename.encode('utf-8')))
|
self.verbose_print('Removing file {0} from archive indexes...'.format(_prepare_unicode_for_print(filename)))
|
||||||
del self.indexes[filename]
|
del self.indexes[filename]
|
||||||
else:
|
else:
|
||||||
raise IOError(errno.ENOENT, 'the requested file {0} does not exist in this archive'.format(filename.encode('utf-8')))
|
raise IOError(errno.ENOENT, 'the requested file {0} does not exist in this archive'.format(_prepare_unicode_for_print(filename)))
|
||||||
|
|
||||||
# Load archive.
|
# Load archive.
|
||||||
def load(self, filename):
|
def load(self, filename):
|
||||||
|
filename = _ensure_unicode(filename)
|
||||||
|
|
||||||
if self.handle is not None:
|
if self.handle is not None:
|
||||||
self.handle.close()
|
self.handle.close()
|
||||||
self.file = filename
|
self.file = filename
|
||||||
|
@ -180,6 +210,8 @@ class RenPyArchive:
|
||||||
|
|
||||||
# Save current state into a new file, merging archive and internal storage, rebuilding indexes, and optionally saving in another format version.
|
# 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):
|
def save(self, filename = None):
|
||||||
|
filename = _ensure_unicode(filename)
|
||||||
|
|
||||||
if filename is None:
|
if filename is None:
|
||||||
filename = self.file
|
filename = self.file
|
||||||
if filename is None:
|
if filename is None:
|
||||||
|
@ -191,7 +223,7 @@ class RenPyArchive:
|
||||||
# Fill our own files structure with the files added or changed in this session.
|
# Fill our own files structure with the files added or changed in this session.
|
||||||
files = self.files
|
files = self.files
|
||||||
# First, read files from the current archive into our files structure.
|
# First, read files from the current archive into our files structure.
|
||||||
for file in self.indexes.keys():
|
for file in list(self.indexes.keys()):
|
||||||
content = self.read(file)
|
content = self.read(file)
|
||||||
# Remove from indexes array once read, add to our own array.
|
# Remove from indexes array once read, add to our own array.
|
||||||
del self.indexes[file]
|
del self.indexes[file]
|
||||||
|
@ -226,14 +258,14 @@ class RenPyArchive:
|
||||||
|
|
||||||
# Write the indexes.
|
# Write the indexes.
|
||||||
self.verbose_print('Writing archive index to archive file...')
|
self.verbose_print('Writing archive index to archive file...')
|
||||||
archive.write(pickle.dumps(indexes, pickle.HIGHEST_PROTOCOL).encode('zlib'))
|
archive.write(codecs.encode(pickle.dumps(indexes, self.PICKLE_PROTOCOL), 'zlib'))
|
||||||
# Now write the header.
|
# Now write the header.
|
||||||
self.verbose_print('Writing header to archive file... (version = RPAv{0})'.format(self.version))
|
self.verbose_print('Writing header to archive file... (version = RPAv{0})'.format(self.version))
|
||||||
archive.seek(0)
|
archive.seek(0)
|
||||||
if self.version == 3:
|
if self.version == 3:
|
||||||
archive.write('RPA-3.0 %016x %08x\n' % (offset, self.key))
|
archive.write(codecs.encode('{}{:016x} {:08x}\n'.format(self.RPA3_MAGIC, offset, self.key)))
|
||||||
else:
|
else:
|
||||||
archive.write('RPA-2.0 %016x\n' % (offset))
|
archive.write(codecs.encode('{}{:016x}\n'.format(self.RPA2_MAGIC, offset)))
|
||||||
# We're done, close it.
|
# We're done, close it.
|
||||||
archive.close()
|
archive.close()
|
||||||
|
|
||||||
|
@ -290,17 +322,17 @@ if __name__ == "__main__":
|
||||||
# Determine output file/directory and input archive
|
# Determine output file/directory and input archive
|
||||||
if arguments.create:
|
if arguments.create:
|
||||||
archive = None
|
archive = None
|
||||||
output = arguments.archive
|
output = _ensure_unicode(arguments.archive)
|
||||||
else:
|
else:
|
||||||
archive = arguments.archive
|
archive = _ensure_unicode(arguments.archive)
|
||||||
if 'outfile' in arguments and arguments.outfile is not None:
|
if 'outfile' in arguments and arguments.outfile is not None:
|
||||||
output = arguments.outfile
|
output = _ensure_unicode(arguments.outfile)
|
||||||
else:
|
else:
|
||||||
# Default output directory for extraction is the current directory.
|
# Default output directory for extraction is the current directory.
|
||||||
if arguments.extract:
|
if arguments.extract:
|
||||||
output = '.'
|
output = '.'
|
||||||
else:
|
else:
|
||||||
output = arguments.archive
|
output = _ensure_unicode(arguments.archive)
|
||||||
|
|
||||||
# Normalize files.
|
# Normalize files.
|
||||||
if len(arguments.files) > 0 and isinstance(arguments.files[0], list):
|
if len(arguments.files) > 0 and isinstance(arguments.files[0], list):
|
||||||
|
@ -308,8 +340,8 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
try:
|
try:
|
||||||
archive = RenPyArchive(archive, padlength=padding, key=key, version=version, verbose=arguments.verbose)
|
archive = RenPyArchive(archive, padlength=padding, key=key, version=version, verbose=arguments.verbose)
|
||||||
except IOError as (errno, errstr):
|
except IOError as e:
|
||||||
sys.stderr.write('Could not open archive file {0} for reading: {1}\n'.format(archive, errstr))
|
print('Could not open archive file {0} for reading: {1}'.format(archive, e), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if arguments.create or arguments.append:
|
if arguments.create or arguments.append:
|
||||||
|
@ -331,32 +363,32 @@ if __name__ == "__main__":
|
||||||
with open(filename, 'rb') as file:
|
with open(filename, 'rb') as file:
|
||||||
archive.add(outfile, file.read())
|
archive.add(outfile, file.read())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write('Could not add file {0} to archive: {1}\n'.format(filename, e))
|
print('Could not add file {0} to archive: {1}'.format(filename, e), file=sys.stderr)
|
||||||
|
|
||||||
# Iterate over the given files to add to archive.
|
# Iterate over the given files to add to archive.
|
||||||
for filename in arguments.files:
|
for filename in arguments.files:
|
||||||
add_file(filename)
|
add_file(_ensure_unicode(filename))
|
||||||
|
|
||||||
# Set version for saving, and save.
|
# Set version for saving, and save.
|
||||||
archive.version = version
|
archive.version = version
|
||||||
try:
|
try:
|
||||||
archive.save(output)
|
archive.save(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write('Could not save archive file: {0}\n'.format(e))
|
print('Could not save archive file: {0}'.format(e), file=sys.stderr)
|
||||||
elif arguments.delete:
|
elif arguments.delete:
|
||||||
# Iterate over the given files to delete from the archive.
|
# Iterate over the given files to delete from the archive.
|
||||||
for filename in arguments.files:
|
for filename in arguments.files:
|
||||||
try:
|
try:
|
||||||
archive.remove(filename)
|
archive.remove(filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write('Could not delete file {0} from archive: {1}\n'.format(filename, e))
|
print('Could not delete file {0} from archive: {1}'.format(filename, e), file=sys.stderr)
|
||||||
|
|
||||||
# Set version for saving, and save.
|
# Set version for saving, and save.
|
||||||
archive.version = version
|
archive.version = version
|
||||||
try:
|
try:
|
||||||
archive.save(output)
|
archive.save(output)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write('Could not save archive file: {0}\n'.format(e))
|
print('Could not save archive file: {0}'.format(e), file=sys.stderr)
|
||||||
elif arguments.extract:
|
elif arguments.extract:
|
||||||
# Either extract the given files, or all files if no files are given.
|
# Either extract the given files, or all files if no files are given.
|
||||||
if len(arguments.files) > 0:
|
if len(arguments.files) > 0:
|
||||||
|
@ -385,14 +417,14 @@ if __name__ == "__main__":
|
||||||
with open(os.path.join(output, outfile), 'wb') as file:
|
with open(os.path.join(output, outfile), 'wb') as file:
|
||||||
file.write(contents)
|
file.write(contents)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write('Could not extract file {0} from archive: {1}\n'.format(filename, e))
|
print('Could not extract file {0} from archive: {1}'.format(filename, e), file=sys.stderr)
|
||||||
elif arguments.list:
|
elif arguments.list:
|
||||||
# Print the sorted file list.
|
# Print the sorted file list.
|
||||||
list = archive.list()
|
list = archive.list()
|
||||||
list.sort()
|
list.sort()
|
||||||
for file in list:
|
for file in list:
|
||||||
print file.encode('utf-8')
|
print(file)
|
||||||
else:
|
else:
|
||||||
print 'No operation given :('
|
print('No operation given :(')
|
||||||
print 'Use {0} --help for usage details.'.format(sys.argv[0])
|
print('Use {0} --help for usage details.'.format(sys.argv[0]))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue