diff --git a/miner/fsd_built.py b/miner/fsd_built.py index f91cc8f..b815fa4 100644 --- a/miner/fsd_built.py +++ b/miner/fsd_built.py @@ -51,8 +51,8 @@ def get_data(self, container_name, language=None, verbose=False, **kwargs): except KeyError: self._container_not_found(container_name) else: - if os.name != 'nt' or struct.calcsize('P') * 8 != 64: - msg = 'need 64-bit python under Windows to execute loader' + if not self._platform_supported(os.name, sys.platform, struct.calcsize('P') * 8): + msg = 'need 64-bit Python on Windows or macOS to execute loader' raise PlatformError(msg) loader_filename = loader_respath.split('/')[-1] loader_info = self._resbrowser.get_file_info(loader_respath) @@ -87,7 +87,11 @@ def _contname_fsdfiles_map(self): loaders = {} datas = {} for resource_path in self._resbrowser.respath_iter(): - m = re.match(r'^app:/bin64/(\w+/)*(?P\w+)Loader.pyd$', resource_path, flags=re.UNICODE) + m = re.match( + r'^app:/(?:.+/)?bin64/(\w+/)*(?P\w+)Loader\.(?:pyd|so)$', + resource_path, + flags=re.UNICODE, + ) if m: loaders[m.group('name').lower()] = resource_path continue @@ -124,6 +128,12 @@ def _compare_files(self, file1_path, file2_path): with open(file1_path, 'rb') as f1, open(file2_path, 'rb') as f2: return f1.read() == f2.read() + @staticmethod + def _platform_supported(os_name, sys_platform, pointer_bits): + if pointer_bits != 64: + return False + return os_name == 'nt' or sys_platform == 'darwin' + class PlatformError(Exception): """Raised when FSD binary miner is used on incorrect platform.""" diff --git a/miner/metadata.py b/miner/metadata.py index 7dbd2f7..ba2bca1 100644 --- a/miner/metadata.py +++ b/miner/metadata.py @@ -44,7 +44,9 @@ def get_data(self, container_name, **kwargs): if container_name != self._container_name: self._container_not_found(container_name) else: - file_info = self._resbrowser.get_file_info('app:/start.ini') + file_info = self._resbrowser.get_file_info( + self._resbrowser.find_resource_path('start.ini', prefix='app:/') + ) field_names = ('field_name', 'field_value') container_data = [] # Read client version diff --git a/tests/test_frontier_resource_paths.py b/tests/test_frontier_resource_paths.py new file mode 100644 index 0000000..62b3405 --- /dev/null +++ b/tests/test_frontier_resource_paths.py @@ -0,0 +1,177 @@ +import configparser +import importlib.util +import os +import sys +import tempfile +import types +import unittest + + +ROOT = os.path.dirname(os.path.dirname(__file__)) + + +class cachedproperty(object): + def __init__(self, method): + self.__method = method + + def __get__(self, instance, owner): + if instance is None: + return self + value = self.__method(instance) + setattr(instance, self.__method.__name__, value) + return value + + +def load_module(module_name, relpath, extra_modules=None): + extra_modules = extra_modules or {} + saved = {} + for name, module in extra_modules.items(): + saved[name] = sys.modules.get(name) + sys.modules[name] = module + try: + spec = importlib.util.spec_from_file_location( + module_name, + os.path.join(ROOT, relpath), + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + finally: + for name, module in extra_modules.items(): + if saved[name] is None: + del sys.modules[name] + else: + sys.modules[name] = saved[name] + + +class FrontierPathFixTests(unittest.TestCase): + def test_resource_browser_supports_nested_frontier_paths(self): + util_module = types.ModuleType('util') + util_module.cachedproperty = cachedproperty + resource_browser = load_module( + 'test_resource_browser', + 'util/resource_browser.py', + {'util': util_module}, + ) + + with tempfile.TemporaryDirectory() as eve_dir: + nested_index = os.path.join( + eve_dir, + 'stillness', + 'EVE.app', + 'Contents', + 'Resources', + 'build', + ) + os.makedirs(nested_index) + expected = os.path.join(nested_index, 'resfileindex.txt') + with open(expected, 'w'): + pass + + browser = resource_browser.ResourceBrowser(eve_dir, 'stillness') + browser._resource_index = { + 'app:/EVE.app/Contents/Resources/build/start.ini': None, + } + + self.assertEqual(browser._resolve_resfileindex_path(), expected) + self.assertEqual( + browser.find_resource_path('start.ini', prefix='app:/'), + 'app:/EVE.app/Contents/Resources/build/start.ini', + ) + + def test_metadata_uses_suffix_lookup_for_start_ini(self): + configparser_module = types.ModuleType('ConfigParser') + configparser_module.ConfigParser = configparser.ConfigParser + miner_base_module = types.ModuleType('miner.base') + + class BaseMiner(object): + def _container_not_found(self, container_name): + raise AssertionError(container_name) + + miner_base_module.BaseMiner = BaseMiner + + metadata_module = load_module( + 'miner.metadata_test', + 'miner/metadata.py', + { + 'ConfigParser': configparser_module, + 'miner.base': miner_base_module, + }, + ) + + class Browser(object): + def __init__(self): + self.lookup_calls = [] + self.info_calls = [] + + def find_resource_path(self, suffix, prefix=None): + self.lookup_calls.append((suffix, prefix)) + return 'app:/EVE.app/Contents/Resources/build/start.ini' + + def get_file_info(self, resource_path): + self.info_calls.append(resource_path) + return types.SimpleNamespace(file_abspath=self.start_ini_path) + + with tempfile.TemporaryDirectory() as temp_dir: + start_ini_path = os.path.join(temp_dir, 'start.ini') + with open(start_ini_path, 'w') as start_ini: + start_ini.write('[main]\nbuild = 12345\n') + + browser = Browser() + browser.start_ini_path = start_ini_path + miner = metadata_module.MetadataMiner(browser) + miner.get_data('metadata') + + self.assertEqual(browser.lookup_calls, [('start.ini', 'app:/')]) + self.assertEqual( + browser.info_calls, + ['app:/EVE.app/Contents/Resources/build/start.ini'], + ) + + def test_fsd_built_accepts_macos_loader_paths(self): + util_module = types.ModuleType('util') + util_module.cachedproperty = cachedproperty + + class EveNormalizer(object): + def run(self, fsd_data, loader_module=None): + return fsd_data + + util_module.EveNormalizer = EveNormalizer + miner_base_module = types.ModuleType('miner.base') + + class BaseMiner(object): + def _container_not_found(self, container_name): + raise AssertionError(container_name) + + miner_base_module.BaseMiner = BaseMiner + + fsd_built_module = load_module( + 'miner.fsd_built_test', + 'miner/fsd_built.py', + { + 'util': util_module, + 'miner.base': miner_base_module, + }, + ) + + class Browser(object): + def respath_iter(self): + return iter(( + 'app:/EVE.app/Contents/Resources/build/bin64/foo/ExampleLoader.so', + 'res:/staticdata/foo/Example.fsdbinary', + )) + + miner = fsd_built_module.FsdBuiltMiner(Browser(), translator=None) + + self.assertTrue(fsd_built_module.FsdBuiltMiner._platform_supported('posix', 'darwin', 64)) + self.assertEqual( + miner._contname_fsdfiles_map['example'], + ( + 'app:/EVE.app/Contents/Resources/build/bin64/foo/ExampleLoader.so', + 'res:/staticdata/foo/Example.fsdbinary', + ), + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/util/resource_browser.py b/util/resource_browser.py index 1ef1922..a598fed 100644 --- a/util/resource_browser.py +++ b/util/resource_browser.py @@ -71,6 +71,15 @@ def get_file_info(self, resource_path): self.__verify_data(data=data, file_info=file_info) return file_info + def find_resource_path(self, suffix, prefix=None): + target_suffix = '/{}'.format(suffix.lstrip('/')) + for resource_path in self._resource_index: + if prefix is not None and not resource_path.startswith(prefix): + continue + if resource_path.endswith(target_suffix): + return resource_path + raise KeyError(suffix) + def __verify_data(self, data, file_info): if len(data) != file_info.file_size: raise FileIntegrityError(u'file size mismatch when reading {}'.format(file_info.resource_path)) @@ -82,7 +91,7 @@ def __verify_data(self, data, file_info): @cachedproperty def _resource_index(self): index = {} - res_index_path = os.path.join(self._eve_path, self._server_alias, 'resfileindex.txt') + res_index_path = self._resolve_resfileindex_path() with open(res_index_path) as f: for resource_path, file_relpath, file_hash, file_size, compressed_size in csv.reader(f): index[resource_path] = FileInfo( @@ -104,6 +113,25 @@ def _resource_index(self): compressed_size=int(compressed_size)) return index + def _resolve_resfileindex_path(self): + legacy_path = os.path.join(self._eve_path, self._server_alias, 'resfileindex.txt') + if os.path.exists(legacy_path): + return legacy_path + + frontier_path = os.path.join( + self._eve_path, + self._server_alias, + 'EVE.app', + 'Contents', + 'Resources', + 'build', + 'resfileindex.txt', + ) + if os.path.exists(frontier_path): + return frontier_path + + return legacy_path + class FileIntegrityError(Exception): pass